Merge branch 'main' into main
|
@ -10,7 +10,7 @@ environment:
|
|||
TEST_OPTIONS:
|
||||
DEPLOY: YES
|
||||
matrix:
|
||||
- PYTHON: C:/Python311
|
||||
- PYTHON: C:/Python312
|
||||
ARCHITECTURE: x86
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||
- PYTHON: C:/Python38-x64
|
||||
|
@ -21,13 +21,11 @@ environment:
|
|||
install:
|
||||
- '%PYTHON%\%EXECUTABLE% --version'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
|
||||
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||
- 7z x pillow-depends.zip -oc:\
|
||||
- 7z x pillow-test-images.zip -oc:\
|
||||
- mv c:\pillow-depends-main c:\pillow-depends
|
||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||
- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.0.0.20230317
|
||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
|
@ -45,7 +43,7 @@ build_script:
|
|||
|
||||
test_script:
|
||||
- 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%
|
||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
||||
|
|
|
@ -23,12 +23,13 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard
|
||||
sway wl-clipboard libopenblas-dev
|
||||
fi
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
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 defusedxml
|
||||
python3 -m pip install olefile
|
||||
|
@ -38,8 +39,8 @@ python3 -m pip install -U pytest-timeout
|
|||
python3 -m pip install pyroma
|
||||
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
# TODO Remove condition when NumPy supports 3.12
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
|
||||
# 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
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
|
@ -47,6 +48,16 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
python3 -m pip install pyqt6
|
||||
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
|
||||
pushd depends && ./install_webp.sh && popd
|
||||
|
||||
|
|
1
.ci/requirements-cibw.txt
Normal file
|
@ -0,0 +1 @@
|
|||
cibuildwheel==2.16.2
|
|
@ -13,7 +13,7 @@ indent_style = space
|
|||
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.yml]
|
||||
[*.{toml,yml}]
|
||||
# Two-space indentation
|
||||
indent_size = 2
|
||||
|
||||
|
|
2
.github/workflows/cifuzz.yml
vendored
|
@ -2,6 +2,8 @@ name: CIFuzz
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths:
|
||||
- ".github/workflows/cifuzz.yml"
|
||||
- "**.c"
|
||||
|
|
6
.github/workflows/docs.yml
vendored
|
@ -2,6 +2,8 @@ name: Docs
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
|
@ -28,10 +30,10 @@ jobs:
|
|||
name: Docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
|
|
4
.github/workflows/lint.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
name: Lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: pre-commit cache
|
||||
uses: actions/cache@v3
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
lint-pre-commit-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
|
|
9
.github/workflows/macos-install.sh
vendored
|
@ -3,8 +3,11 @@
|
|||
set -e
|
||||
|
||||
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
|
||||
# TODO Update condition when cffi supports 3.13
|
||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
|
||||
|
||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install olefile
|
||||
|
@ -13,8 +16,8 @@ python3 -m pip install -U pytest-cov
|
|||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
|
||||
# TODO Remove condition when NumPy supports 3.12
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
|
||||
# TODO Update condition when NumPy supports 3.13
|
||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
2
.github/workflows/stale.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Check issues"
|
||||
uses: actions/stale@v8
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "Awaiting OP Action"
|
||||
|
|
16
.github/workflows/test-cygwin.yml
vendored
|
@ -2,13 +2,23 @@ name: Test Cygwin
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -36,7 +46,7 @@ jobs:
|
|||
git config --global core.autocrlf input
|
||||
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: cygwin/cygwin-install-action@v4
|
||||
|
@ -102,10 +112,10 @@ jobs:
|
|||
run: |
|
||||
bash.exe .ci/install.sh
|
||||
|
||||
- name: Install latest NumPy
|
||||
- name: Upgrade NumPy
|
||||
shell: dash.exe -l "{0}"
|
||||
run: |
|
||||
python3 -m pip install -U numpy
|
||||
python3 -m pip install -U "numpy<1.26"
|
||||
|
||||
- name: Build
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
|
|
14
.github/workflows/test-docker.yml
vendored
|
@ -2,13 +2,23 @@ name: Test Docker
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -41,8 +51,8 @@ jobs:
|
|||
debian-11-bullseye-amd64,
|
||||
debian-12-bookworm-x86,
|
||||
debian-12-bookworm-amd64,
|
||||
fedora-37-amd64,
|
||||
fedora-38-amd64,
|
||||
fedora-39-amd64,
|
||||
gentoo,
|
||||
ubuntu-20.04-focal-amd64,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
|
@ -59,7 +69,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
12
.github/workflows/test-mingw.yml
vendored
|
@ -2,13 +2,23 @@ name: Test MinGW
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -34,7 +44,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up shell
|
||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||
|
|
6
.github/workflows/test-valgrind.yml
vendored
|
@ -1,9 +1,11 @@
|
|||
name: Test Valgrind
|
||||
|
||||
# like the docker tests, but running valgrind only on *.c/*.h changes.
|
||||
# like the Docker tests, but running valgrind only on *.c/*.h changes.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths:
|
||||
- ".github/workflows/test-valgrind.yml"
|
||||
- "**.c"
|
||||
|
@ -37,7 +39,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
71
.github/workflows/test-windows.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
|||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -24,7 +32,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
|
@ -32,41 +40,42 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: python-pillow/test-images
|
||||
path: Tests\test-images
|
||||
|
||||
# sets env: pythonLocation
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
cache: pip
|
||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
- name: Print build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
|
||||
run: 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 olefile pyroma
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
run: |
|
||||
7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||
echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH
|
||||
choco install nasm --no-progress
|
||||
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
|
||||
|
||||
# Install extra test images
|
||||
|
@ -158,7 +167,6 @@ jobs:
|
|||
- name: Build Pillow
|
||||
run: |
|
||||
$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 ."
|
||||
& $env:pythonLocation\python.exe selftest.py --installed
|
||||
shell: pwsh
|
||||
|
@ -200,47 +208,6 @@ jobs:
|
|||
flags: GHA_Windows
|
||||
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:
|
||||
permissions:
|
||||
contents: none
|
||||
|
|
18
.github/workflows/test.yml
vendored
|
@ -2,13 +2,23 @@ name: Test
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -31,7 +41,8 @@ jobs:
|
|||
python-version: [
|
||||
"pypy3.10",
|
||||
"pypy3.9",
|
||||
"3.12-dev",
|
||||
"3.13",
|
||||
"3.12",
|
||||
"3.11",
|
||||
"3.10",
|
||||
"3.9",
|
||||
|
@ -48,12 +59,13 @@ jobs:
|
|||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
cache: pip
|
||||
cache-dependency-path: ".ci/*.sh"
|
||||
|
||||
|
|
151
.github/workflows/wheels-dependencies.sh
vendored
Executable 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
|
22
.github/workflows/wheels-test.ps1
vendored
Normal 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
|
@ -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
|
206
.github/workflows/wheels.yml
vendored
Normal file
|
@ -0,0 +1,206 @@
|
|||
name: Wheels
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: "macOS x86_64"
|
||||
os: macos-latest
|
||||
archs: x86_64
|
||||
macosx_deployment_target: "10.10"
|
||||
- name: "macOS arm64"
|
||||
os: macos-latest
|
||||
archs: arm64
|
||||
macosx_deployment_target: "11.0"
|
||||
- name: "manylinux2014 and musllinux x86_64"
|
||||
os: ubuntu-latest
|
||||
archs: x86_64
|
||||
- name: "manylinux_2_28 x86_64"
|
||||
os: ubuntu-latest
|
||||
archs: 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: Build wheels
|
||||
run: |
|
||||
python3 -m pip install -r .ci/requirements-cibw.txt
|
||||
python3 -m cibuildwheel --output-dir wheelhouse
|
||||
env:
|
||||
CIBW_ARCHS: ${{ matrix.archs }}
|
||||
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@v3
|
||||
with:
|
||||
name: dist
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
windows:
|
||||
name: Windows ${{ matrix.arch }}
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86
|
||||
cibw_arch: x86
|
||||
- arch: x64
|
||||
cibw_arch: AMD64
|
||||
- arch: ARM64
|
||||
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: 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 -m pip install -r .ci/requirements-cibw.txt
|
||||
|
||||
# Cannot cross-compile FriBiDi (only used for tests)
|
||||
$FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
|
||||
if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
|
||||
& python.exe winbuild\build_prepare.py -v @FLAGS
|
||||
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_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@v3
|
||||
with:
|
||||
name: dist
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
- name: Prepare to upload FriBiDi
|
||||
if: "matrix.arch != 'ARM64'"
|
||||
run: |
|
||||
mkdir fribidi\${{ matrix.arch }}
|
||||
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
|
||||
shell: cmd
|
||||
|
||||
- name: Upload fribidi.dll
|
||||
if: "matrix.arch != 'ARM64'"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: fribidi
|
||||
path: 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@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/*.tar.gz
|
||||
|
||||
success:
|
||||
permissions:
|
||||
contents: none
|
||||
needs: [build, windows, sdist]
|
||||
runs-on: ubuntu-latest
|
||||
name: Wheels Successful
|
||||
steps:
|
||||
- name: Success
|
||||
run: echo Wheels Successful
|
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "multibuild"]
|
||||
path = wheels/multibuild
|
||||
url = https://github.com/multi-build/multibuild.git
|
|
@ -1,14 +1,14 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.7.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.11.0
|
||||
hooks:
|
||||
- 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
|
||||
rev: 1.7.5
|
||||
|
@ -17,50 +17,42 @@ repos:
|
|||
args: [--severity-level=high]
|
||||
files: ^src/
|
||||
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: v1.5.0
|
||||
hooks:
|
||||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.3
|
||||
rev: v1.5.4
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
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]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-merge-conflict
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^Tests/images/
|
||||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.6.7
|
||||
rev: v0.9.0
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 0.13.0
|
||||
rev: 1.5.3
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.13
|
||||
rev: v0.15
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
|
|
52
.travis.yml
Normal file
|
@ -0,0 +1,52 @@
|
|||
if: tag IS present OR type = api
|
||||
|
||||
env:
|
||||
global:
|
||||
- CIBW_ARCHS=aarch64
|
||||
- CIBW_SKIP=pp38-*
|
||||
|
||||
language: python
|
||||
# Default Python version is usually 3.6
|
||||
python: "3.12"
|
||||
dist: jammy
|
||||
services: docker
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- name: "manylinux2014 aarch64"
|
||||
os: linux
|
||||
arch: arm64
|
||||
env:
|
||||
- CIBW_BUILD="*manylinux*"
|
||||
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
|
||||
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
|
||||
- name: "manylinux_2_28 aarch64"
|
||||
os: linux
|
||||
arch: arm64
|
||||
env:
|
||||
- CIBW_BUILD="*manylinux*"
|
||||
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
|
||||
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
|
||||
- name: "musllinux aarch64"
|
||||
os: linux
|
||||
arch: arm64
|
||||
env:
|
||||
- CIBW_BUILD="*musllinux*"
|
||||
|
||||
install:
|
||||
- python3 -m pip install -r .ci/requirements-cibw.txt
|
||||
|
||||
script:
|
||||
- python3 -m cibuildwheel --output-dir wheelhouse
|
||||
- ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
|
||||
|
||||
# Upload wheels to GitHub Releases
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_RELEASE_TOKEN
|
||||
file_glob: true
|
||||
file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
|
||||
on:
|
||||
repo: python-pillow/Pillow
|
||||
tags: true
|
||||
skip_cleanup: true
|
168
CHANGES.rst
|
@ -2,9 +2,120 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
10.1.0 (unreleased)
|
||||
10.2.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- 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)
|
||||
-------------------
|
||||
|
||||
- Added TrueType default font to allow for different sizes #7354
|
||||
[radarhere]
|
||||
|
||||
- Fixed invalid argument warning #7442
|
||||
[radarhere]
|
||||
|
||||
- Added ImageOps cover method #7412
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Catch struct.error from truncated EXIF when reading JPEG DPI #7458
|
||||
[radarhere]
|
||||
|
||||
- Consider default image when selecting mode for PNG save_all #7437
|
||||
[radarhere]
|
||||
|
||||
- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303
|
||||
[radarhere]
|
||||
|
||||
- Added CMYK to RGB unpacker #7310
|
||||
[radarhere]
|
||||
|
||||
- Improved flexibility of XMP parsing #7274
|
||||
[radarhere]
|
||||
|
||||
- Support reading 8-bit YCbCr TIFF images #7415
|
||||
[radarhere]
|
||||
|
||||
- Allow saving I;16B images as PNG #7302
|
||||
[radarhere]
|
||||
|
||||
- Corrected drawing I;16 points and writing I;16 text #7257
|
||||
[radarhere]
|
||||
|
||||
- Set blue channel to 128 for BC5S #7413
|
||||
[radarhere]
|
||||
|
||||
- Increase flexibility when reading IPTC fields #7319
|
||||
[radarhere]
|
||||
|
||||
- Set C palette to be empty by default #7289
|
||||
[radarhere]
|
||||
|
||||
- Added gs_binary to control Ghostscript use on all platforms #7392
|
||||
[radarhere]
|
||||
|
||||
- Read bounding box information from the trailer of EPS files if specified #7382
|
||||
[nopperl, radarhere]
|
||||
|
||||
- Added reading 8-bit color DDS images #7426
|
||||
[radarhere]
|
||||
|
||||
- Added has_transparency_data #7420
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Fixed bug when reading BC5S DDS images #7401
|
||||
[radarhere]
|
||||
|
||||
- Prevent TIFF orientation from being applied more than once #7383
|
||||
[radarhere]
|
||||
|
||||
- Use previous pixel alpha for QOI_OP_RGB #7357
|
||||
[radarhere]
|
||||
|
||||
- Added BC5U reading #7358
|
||||
[radarhere]
|
||||
|
||||
- Allow getpixel() to accept a list #7355
|
||||
[radarhere, homm]
|
||||
|
||||
|
@ -17,9 +128,6 @@ Changelog (Pillow)
|
|||
- Added session type check for Linux in ImageGrab.grabclipboard() #7332
|
||||
[TheNooB2706, radarhere, hugovk]
|
||||
|
||||
- Read WebP duration after opening #7311
|
||||
[k128, radarhere]
|
||||
|
||||
- Allow "loop=None" when saving GIF images #7329
|
||||
[radarhere]
|
||||
|
||||
|
@ -44,6 +152,15 @@ Changelog (Pillow)
|
|||
- Fix missing symbols when libtiff depends on libjpeg #7270
|
||||
[heitbaum]
|
||||
|
||||
10.0.1 (2023-09-15)
|
||||
-------------------
|
||||
|
||||
- Updated libwebp to 1.3.2 #7395
|
||||
[radarhere]
|
||||
|
||||
- Updated zlib to 1.3 #7344
|
||||
[radarhere]
|
||||
|
||||
10.0.0 (2023-07-01)
|
||||
-------------------
|
||||
|
||||
|
@ -2119,7 +2236,7 @@ Changelog (Pillow)
|
|||
- Cache EXIF information #3498
|
||||
[Glandos]
|
||||
|
||||
- Added transparency for all PNG greyscale modes #3744
|
||||
- Added transparency for all PNG grayscale modes #3744
|
||||
[radarhere]
|
||||
|
||||
- Fix deprecation warnings in Python 3.8 #3749
|
||||
|
@ -4621,7 +4738,7 @@ Changelog (Pillow)
|
|||
- Fix Bicubic interpolation #970
|
||||
[homm]
|
||||
|
||||
- Support for 4-bit greyscale TIFF images #980
|
||||
- Support for 4-bit grayscale TIFF images #980
|
||||
[hugovk]
|
||||
|
||||
- Updated manifest #957
|
||||
|
@ -5771,8 +5888,8 @@ http://svn.effbot.org/public/pil/
|
|||
a polyline, independent of line angle.
|
||||
|
||||
- Fixed bearing calculation and clipping in the ImageFont truetype
|
||||
renderer; this could lead to clipped text, or crashes in the low-
|
||||
level _imagingft module. (based on input from Adam Twardoch and
|
||||
renderer; this could lead to clipped text, or crashes in the low-level
|
||||
_imagingft module. (based on input from Adam Twardoch and
|
||||
others).
|
||||
|
||||
- Added ImageQt wrapper module, for converting PIL Image objects to
|
||||
|
@ -5853,8 +5970,7 @@ http://svn.effbot.org/public/pil/
|
|||
1.1.5c2 and 1.1.5 final
|
||||
-----------------------
|
||||
|
||||
- Added experimental PERSPECTIVE transform method (from Jeff Breiden-
|
||||
bach).
|
||||
- Added experimental PERSPECTIVE transform method (from Jeff Breidenbach).
|
||||
|
||||
1.1.5c1
|
||||
-------
|
||||
|
@ -5926,8 +6042,8 @@ http://svn.effbot.org/public/pil/
|
|||
|
||||
- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA".
|
||||
|
||||
- Added "getcolors()" method. This is similar to the existing histo-
|
||||
gram method, but looks at color values instead of individual layers,
|
||||
- Added "getcolors()" method. This is similar to the existing histogram
|
||||
method, but looks at color values instead of individual layers,
|
||||
and returns an unsorted list of (count, color) tuples.
|
||||
|
||||
By default, the method returns None if finds more than 256 colors.
|
||||
|
@ -6143,8 +6259,8 @@ http://svn.effbot.org/public/pil/
|
|||
|
||||
- Added limited support for "bitfield compression" in BMP files
|
||||
and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This
|
||||
also fixes a problem with ImageGrab module when copying screen-
|
||||
dumps from the clipboard on 15/16/32-bit displays.
|
||||
also fixes a problem with ImageGrab module when copying screendumps
|
||||
from the clipboard on 15/16/32-bit displays.
|
||||
|
||||
- Added experimental WAL (Quake 2 textures) loader. To use this
|
||||
loader, import WalImageFile and call the "open" method in that
|
||||
|
@ -6255,8 +6371,8 @@ http://svn.effbot.org/public/pil/
|
|||
1.1.3 final
|
||||
-----------
|
||||
|
||||
- Made setup.py look for old versions of zlib. For some back-
|
||||
ground, see: https://zlib.net/advisory-2002-03-11.txt
|
||||
- Made setup.py look for old versions of zlib. For some background,
|
||||
see: https://zlib.net/advisory-2002-03-11.txt
|
||||
|
||||
1.1.3c2
|
||||
-------
|
||||
|
@ -6447,8 +6563,8 @@ http://svn.effbot.org/public/pil/
|
|||
supports all major PIL image modes (including F and I).
|
||||
|
||||
- The ImageFile module now includes a Parser class, which can
|
||||
be used to incrementally decode an image file (while down-
|
||||
loading it from the net, for example). See the handbook for
|
||||
be used to incrementally decode an image file (while downloading
|
||||
it from the net, for example). See the handbook for
|
||||
details.
|
||||
|
||||
- "show" now converts non-standard modes to "L" or "RGB" (as
|
||||
|
@ -6586,8 +6702,8 @@ http://svn.effbot.org/public/pil/
|
|||
|
||||
- The Image "transform" method now supports Image.QUAD transforms.
|
||||
The data argument is an 8-tuple giving the upper left, lower
|
||||
left, lower right, and upper right corner of the source quadri-
|
||||
lateral. Also added Image.MESH transform which takes a list
|
||||
left, lower right, and upper right corner of the source quadrilateral.
|
||||
Also added Image.MESH transform which takes a list
|
||||
of quadrilaterals.
|
||||
|
||||
- The Image "resize", "rotate", and "transform" methods now support
|
||||
|
@ -6697,7 +6813,7 @@ The test suite includes 750 individual tests.
|
|||
|
||||
- You can now convert directly between all modes supported by
|
||||
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.
|
||||
|
||||
- Added a "dither" option to "convert". By default, "convert"
|
||||
|
@ -6775,13 +6891,13 @@ The test suite includes 530 individual tests.
|
|||
- Fixed "paste" to allow a mask also for mode "F" images.
|
||||
|
||||
- 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.
|
||||
|
||||
- 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
|
||||
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.
|
||||
|
||||
- Fixed a potential buffer overrun in the GIF encoder.
|
||||
|
@ -6812,8 +6928,8 @@ The test suite includes 400 individual tests.
|
|||
neither "short", "int" nor "long" are 32-bit wide.
|
||||
|
||||
- Added file= and data= keyword arguments to PhotoImage and BitmapImage.
|
||||
This allows you to use them as drop-in replacements for the corre-
|
||||
sponding Tkinter classes.
|
||||
This allows you to use them as drop-in replacements for the corresponding
|
||||
Tkinter classes.
|
||||
|
||||
- Removed bogus references to the crack coder (ImagingCrack).
|
||||
|
||||
|
@ -7085,7 +7201,7 @@ The test suite includes 400 individual tests.
|
|||
drawing capabilities can be used to render vector and metafile
|
||||
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).
|
||||
|
||||
- Some minor improvements to the sample scripts provided in the
|
||||
|
|
|
@ -5,8 +5,10 @@ include *.md
|
|||
include *.py
|
||||
include *.rst
|
||||
include *.sh
|
||||
include *.toml
|
||||
include *.txt
|
||||
include *.yaml
|
||||
include .flake8
|
||||
include LICENSE
|
||||
include Makefile
|
||||
include tox.ini
|
||||
|
@ -29,3 +31,4 @@ global-exclude .git*
|
|||
global-exclude *.pyc
|
||||
global-exclude *.so
|
||||
prune .ci
|
||||
prune wheels
|
||||
|
|
8
Makefile
|
@ -49,7 +49,7 @@ help:
|
|||
@echo " install make and install"
|
||||
@echo " install-coverage make and install with C coverage"
|
||||
@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 " test run tests on installed Pillow"
|
||||
|
||||
|
@ -118,6 +118,6 @@ lint:
|
|||
.PHONY: lint-fix
|
||||
lint-fix:
|
||||
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 --target-version py38 .
|
||||
python3 -m isort .
|
||||
python3 -m black .
|
||||
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
|
||||
python3 -m ruff --fix .
|
||||
|
|
14
README.md
|
@ -45,12 +45,12 @@ As of 2019, Pillow development is
|
|||
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
|
||||
alt="AppVeyor CI build status (Windows)"
|
||||
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
|
||||
<a href="https://github.com/python-pillow/pillow-wheels/actions"><img
|
||||
alt="GitHub Actions wheels build status (Wheels)"
|
||||
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a>
|
||||
<a href="https://app.travis-ci.com/github/python-pillow/pillow-wheels"><img
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
|
||||
alt="GitHub Actions build status (Wheels)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
|
||||
<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-wheels/main.svg?label=aarch64%20wheels"></a>
|
||||
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
|
||||
alt="Code coverage"
|
||||
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
|
||||
|
@ -74,9 +74,9 @@ As of 2019, Pillow development is
|
|||
<a href="https://pypi.org/project/Pillow/"><img
|
||||
alt="Number of PyPI downloads"
|
||||
src="https://img.shields.io/pypi/dm/pillow.svg"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img
|
||||
<a href="https://www.bestpractices.dev/projects/6331"><img
|
||||
alt="OpenSSF Best Practices"
|
||||
src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a>
|
||||
src="https://www.bestpractices.dev/projects/6331/badge"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
38
RELEASING.md
|
@ -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
|
||||
* [ ] 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 that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions.
|
||||
* [ ] 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.
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Update `CHANGES.rst`.
|
||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||
|
@ -20,12 +20,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
git tag 5.2.0
|
||||
git push --tags
|
||||
```
|
||||
* [ ] Create and check source distribution:
|
||||
```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.:
|
||||
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Check and upload all source and binary distributions e.g.:
|
||||
```bash
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.0*
|
||||
|
@ -59,8 +55,8 @@ Released as needed for security, installation or critical bug fixes.
|
|||
```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.:
|
||||
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Check and upload all source and binary distributions e.g.:
|
||||
```bash
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.1*
|
||||
|
@ -90,34 +86,22 @@ Released as needed privately to individual vendors for critical security-related
|
|||
```bash
|
||||
make sdist
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||
```bash
|
||||
git push origin 2.5.x
|
||||
```
|
||||
|
||||
## Binary Distributions
|
||||
## Source and Binary Distributions
|
||||
|
||||
### macOS and Linux
|
||||
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
||||
```bash
|
||||
git clone https://github.com/python-pillow/pillow-wheels
|
||||
cd pillow-wheels
|
||||
./update-pillow-tag.sh [[release tag]]
|
||||
```
|
||||
* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
|
||||
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo:
|
||||
```bash
|
||||
gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels
|
||||
```
|
||||
|
||||
### Windows
|
||||
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
||||
* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.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
|
||||
# select dist
|
||||
```
|
||||
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
|
||||
and copy into `dist`.
|
||||
|
||||
## Publicize Release
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ def test_direct():
|
|||
|
||||
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_set, "PyAccess - set", im.size, access)
|
||||
timer(iterate_get, "C-api - get", im.size, caccess)
|
||||
|
|
0
Tests/check_j2k_leaks.py
Executable file → Normal file
41
Tests/check_wheel.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
import sys
|
||||
|
||||
from PIL import features
|
||||
|
||||
|
||||
def test_wheel_modules():
|
||||
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():
|
||||
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
|
||||
|
||||
assert set(features.get_supported_codecs()) == expected_codecs
|
||||
|
||||
|
||||
def test_wheel_features():
|
||||
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
|
|
@ -5,6 +5,7 @@ Helper functions.
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
|
@ -91,11 +92,11 @@ def assert_image_equal(a, b, msg=None):
|
|||
if HAS_UPLOADER:
|
||||
try:
|
||||
url = test_image_results.upload(a, b)
|
||||
logger.error(f"Url for test images: {url}")
|
||||
logger.error("URL for test images: %s", url)
|
||||
except Exception:
|
||||
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):
|
||||
|
@ -126,7 +127,7 @@ def assert_image_similar(a, b, epsilon, msg=None):
|
|||
if HAS_UPLOADER:
|
||||
try:
|
||||
url = test_image_results.upload(a, b)
|
||||
logger.error(f"Url for test images: {url}")
|
||||
logger.exception("URL for test images: %s", url)
|
||||
except Exception:
|
||||
pass
|
||||
raise e
|
||||
|
@ -258,11 +259,21 @@ def hopper(mode=None, cache={}):
|
|||
|
||||
|
||||
def djpeg_available():
|
||||
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
|
||||
|
||||
|
||||
def cjpeg_available():
|
||||
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
|
||||
|
||||
|
||||
def netpbm_available():
|
||||
|
|
|
@ -22,4 +22,3 @@ and that the name of ICC shall not be used in advertising or publicity
|
|||
pertaining to distribution of the software without specific, written
|
||||
prior permission. ICC makes no representations about the suitability
|
||||
of this software for any purpose.
|
||||
|
||||
|
|
Before Width: | Height: | Size: 331 B After Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 668 B After Width: | Height: | Size: 668 B |
BIN
Tests/images/bc1.dds
Executable file
BIN
Tests/images/bc1_typeless.dds
Executable file
BIN
Tests/images/bc4_typeless.dds
Normal file
BIN
Tests/images/bc4_unorm.dds
Normal file
BIN
Tests/images/bc4_unorm.png
Normal file
After Width: | Height: | Size: 982 B |
BIN
Tests/images/bc4u.dds
Normal file
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 95 KiB |
BIN
Tests/images/bc5u.dds
Normal file
BIN
Tests/images/default_font_freetype.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
Tests/images/imagedraw_default_font_size.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 180 B |
0
Tests/images/negative_size.ppm
Executable file → Normal file
BIN
Tests/images/palette.dds
Normal file
BIN
Tests/images/truncated_exif_dpi.jpg
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
Tests/images/unsupported_bitcount_luminance.dds
Normal file
BIN
Tests/images/unsupported_bitcount_rgb.dds
Normal file
BIN
Tests/images/xmp_no_prefix.jpg
Normal file
After Width: | Height: | Size: 788 B |
BIN
Tests/images/xmp_padded.jpg
Normal file
After Width: | Height: | Size: 778 B |
BIN
Tests/images/zero_bb_eof_before_boundingbox.eps
Normal file
BIN
Tests/images/zero_bb_trailer.eps
Normal file
|
@ -15,7 +15,7 @@
|
|||
#
|
||||
################################################################################
|
||||
|
||||
python3 setup.py build --build-base=/tmp/build install
|
||||
python3 -m pip install .
|
||||
|
||||
# Build fuzzers in $OUT.
|
||||
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
|
||||
|
|
|
@ -231,13 +231,13 @@ def test_apng_mode():
|
|||
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_greyscale.png") as im:
|
||||
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
|
||||
assert im.mode == "L"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == 128
|
||||
assert im.getpixel((64, 32)) == 255
|
||||
|
||||
with Image.open("Tests/images/apng/mode_greyscale_alpha.png") as im:
|
||||
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
|
||||
assert im.mode == "LA"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (128, 191)
|
||||
|
@ -350,15 +350,13 @@ def test_apng_save(tmp_path):
|
|||
im.load()
|
||||
assert not im.is_animated
|
||||
assert im.n_frames == 1
|
||||
assert im.get_format_mimetype() == "image/apng"
|
||||
assert im.get_format_mimetype() == "image/png"
|
||||
assert im.info.get("default_image") is None
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/single_frame_default.png") as im:
|
||||
frames = []
|
||||
for frame_im in ImageSequence.Iterator(im):
|
||||
frames.append(frame_im.copy())
|
||||
frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)]
|
||||
frames[0].save(
|
||||
test_file, save_all=True, default_image=True, append_images=frames[1:]
|
||||
)
|
||||
|
@ -450,26 +448,29 @@ def test_apng_save_duration_loop(tmp_path):
|
|||
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.load()
|
||||
assert im.n_frames == 1
|
||||
assert im.info.get("duration") == 750
|
||||
assert "duration" not in im.info
|
||||
|
||||
different_frame = Image.new("RGBA", (128, 64))
|
||||
frame.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[frame, different_frame],
|
||||
duration=[500, 100, 150],
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
assert im.n_frames == 2
|
||||
assert im.info["duration"] == 600
|
||||
|
||||
im.seek(1)
|
||||
assert im.info["duration"] == 150
|
||||
|
||||
# test info duration
|
||||
frame.info["duration"] = 750
|
||||
frame.save(test_file, save_all=True)
|
||||
frame.info["duration"] = 300
|
||||
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
|
||||
with Image.open(test_file) as im:
|
||||
assert im.info.get("duration") == 750
|
||||
|
||||
|
||||
def test_apng_save_duplicate_duration(tmp_path):
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
frame = Image.new("RGB", (1, 1))
|
||||
|
||||
# Test a single duration is correctly combined across duplicate frames
|
||||
frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
|
||||
with Image.open(test_file) as im:
|
||||
assert im.n_frames == 1
|
||||
assert im.info.get("duration") == 1500
|
||||
assert im.n_frames == 2
|
||||
assert im.info["duration"] == 600
|
||||
|
||||
|
||||
def test_apng_save_disposal(tmp_path):
|
||||
|
@ -673,10 +674,17 @@ def test_seek_after_close():
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
||||
def test_different_modes_in_later_frames(mode, tmp_path):
|
||||
@pytest.mark.parametrize("default_image", (True, False))
|
||||
@pytest.mark.parametrize("duplicate", (True, False))
|
||||
def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
im = Image.new("L", (1, 1))
|
||||
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
|
||||
im.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
default_image=default_image,
|
||||
append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
|
||||
)
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert reloaded.mode == mode
|
||||
|
|
|
@ -159,7 +159,7 @@ def test_rle8():
|
|||
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||
|
||||
with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im:
|
||||
with Image.open("Tests/images/hopper_rle8_grayscale.bmp") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
|
||||
|
||||
# This test image has been manually hexedited
|
||||
|
|
|
@ -12,10 +12,16 @@ TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
|
|||
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
|
||||
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
|
||||
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
|
||||
TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds"
|
||||
TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds"
|
||||
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
|
||||
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
||||
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
||||
TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds"
|
||||
TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds"
|
||||
TEST_FILE_BC4U = "Tests/images/bc4u.dds"
|
||||
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
|
||||
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
|
||||
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
|
||||
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
|
||||
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
|
||||
|
@ -28,11 +34,20 @@ TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
|
|||
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
||||
|
||||
|
||||
def test_sanity_dxt1():
|
||||
"""Check DXT1 images can be opened"""
|
||||
@pytest.mark.parametrize(
|
||||
"image_path",
|
||||
(
|
||||
TEST_FILE_DXT1,
|
||||
# hexeditted to use DX10 FourCC
|
||||
TEST_FILE_DX10_BC1,
|
||||
TEST_FILE_DX10_BC1_TYPELESS,
|
||||
),
|
||||
)
|
||||
def test_sanity_dxt1_bc1(image_path):
|
||||
"""Check DXT1 and BC1 images can be opened"""
|
||||
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
|
||||
target = target.convert("RGBA")
|
||||
with Image.open(TEST_FILE_DXT1) as im:
|
||||
with Image.open(image_path) as im:
|
||||
im.load()
|
||||
|
||||
assert im.format == "DDS"
|
||||
|
@ -68,10 +83,18 @@ def test_sanity_dxt5():
|
|||
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
|
||||
|
||||
|
||||
def test_sanity_ati1():
|
||||
"""Check ATI1 images can be opened"""
|
||||
@pytest.mark.parametrize(
|
||||
"image_path",
|
||||
(
|
||||
TEST_FILE_ATI1,
|
||||
# hexeditted to use BC4U FourCC
|
||||
TEST_FILE_BC4U,
|
||||
),
|
||||
)
|
||||
def test_sanity_ati1_bc4u(image_path):
|
||||
"""Check ATI1 and BC4U images can be opened"""
|
||||
|
||||
with Image.open(TEST_FILE_ATI1) as im:
|
||||
with Image.open(image_path) as im:
|
||||
im.load()
|
||||
|
||||
assert im.format == "DDS"
|
||||
|
@ -81,10 +104,39 @@ def test_sanity_ati1():
|
|||
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
|
||||
|
||||
|
||||
def test_sanity_ati2():
|
||||
"""Check ATI2 images can be opened"""
|
||||
@pytest.mark.parametrize(
|
||||
"image_path",
|
||||
(
|
||||
TEST_FILE_DX10_BC4_UNORM,
|
||||
# hexeditted to be typeless
|
||||
TEST_FILE_DX10_BC4_TYPELESS,
|
||||
),
|
||||
)
|
||||
def test_dx10_bc4(image_path):
|
||||
"""Check DX10 BC4 images can be opened"""
|
||||
|
||||
with Image.open(TEST_FILE_ATI2) as im:
|
||||
with Image.open(image_path) as im:
|
||||
im.load()
|
||||
|
||||
assert im.format == "DDS"
|
||||
assert im.mode == "L"
|
||||
assert im.size == (64, 64)
|
||||
|
||||
assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"image_path",
|
||||
(
|
||||
TEST_FILE_ATI2,
|
||||
# hexeditted to use BC5U FourCC
|
||||
TEST_FILE_BC5U,
|
||||
),
|
||||
)
|
||||
def test_sanity_ati2_bc5u(image_path):
|
||||
"""Check ATI2 and BC5U images can be opened"""
|
||||
|
||||
with Image.open(image_path) as im:
|
||||
im.load()
|
||||
|
||||
assert im.format == "DDS"
|
||||
|
@ -190,12 +242,6 @@ def test_dx10_r8g8b8a8_unorm_srgb():
|
|||
)
|
||||
|
||||
|
||||
def test_unimplemented_dxgi_format():
|
||||
with pytest.raises(NotImplementedError):
|
||||
with Image.open("Tests/images/unimplemented_dxgi_format.dds"):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "size", "test_file"),
|
||||
[
|
||||
|
@ -289,9 +335,34 @@ def test_dxt5_colorblock_alpha_issue_4142():
|
|||
assert px[2] != 0
|
||||
|
||||
|
||||
def test_unimplemented_pixel_format():
|
||||
def test_palette():
|
||||
with Image.open("Tests/images/palette.dds") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
(
|
||||
"Tests/images/unsupported_bitcount_rgb.dds",
|
||||
"Tests/images/unsupported_bitcount_luminance.dds",
|
||||
),
|
||||
)
|
||||
def test_unsupported_bitcount(test_file):
|
||||
with pytest.raises(OSError):
|
||||
with Image.open(test_file):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
(
|
||||
"Tests/images/unimplemented_dxgi_format.dds",
|
||||
"Tests/images/unimplemented_pfflags.dds",
|
||||
),
|
||||
)
|
||||
def test_not_implemented(test_file):
|
||||
with pytest.raises(NotImplementedError):
|
||||
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
|
||||
with Image.open(test_file):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from .helper import (
|
|||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
is_win32,
|
||||
mark_if_feature_version,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
@ -98,6 +99,20 @@ def test_load():
|
|||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
|
||||
|
||||
def test_binary():
|
||||
if HAS_GHOSTSCRIPT:
|
||||
assert EpsImagePlugin.gs_binary is not None
|
||||
else:
|
||||
assert EpsImagePlugin.gs_binary is False
|
||||
|
||||
if not is_win32():
|
||||
assert EpsImagePlugin.gs_windows_binary is None
|
||||
elif not HAS_GHOSTSCRIPT:
|
||||
assert EpsImagePlugin.gs_windows_binary is False
|
||||
else:
|
||||
assert EpsImagePlugin.gs_windows_binary is not None
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
with pytest.raises(SyntaxError):
|
||||
|
@ -404,3 +419,18 @@ def test_timeout(test_file):
|
|||
with pytest.raises(Image.UnidentifiedImageError):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
||||
def test_bounding_box_in_trailer():
|
||||
# Check bounding boxes are parsed in the same way
|
||||
# when specified in the header and the trailer
|
||||
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
|
||||
FILE1
|
||||
) as header_image:
|
||||
assert trailer_image.size == header_image.size
|
||||
|
||||
|
||||
def test_eof_before_bounding_box():
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||
pass
|
||||
|
|
|
@ -205,14 +205,14 @@ def test_optimize_full_l():
|
|||
|
||||
|
||||
def test_optimize_if_palette_can_be_reduced_by_half():
|
||||
with Image.open("Tests/images/test.colors.gif") as im:
|
||||
# Reduce dimensions because original is too big for _get_optimize()
|
||||
im = im.resize((591, 443))
|
||||
im_rgb = im.convert("RGB")
|
||||
im = Image.new("P", (8, 1))
|
||||
im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
|
||||
for i in range(8):
|
||||
im.putpixel((i, 0), (i + 1, 0, 0))
|
||||
|
||||
for optimize, colors in ((False, 256), (True, 8)):
|
||||
out = BytesIO()
|
||||
im_rgb.save(out, "GIF", optimize=optimize)
|
||||
im.save(out, "GIF", optimize=optimize)
|
||||
with Image.open(out) as reloaded:
|
||||
assert len(reloaded.palette.palette) // 3 == colors
|
||||
|
||||
|
@ -590,7 +590,7 @@ def test_save_dispose(tmp_path):
|
|||
def test_dispose2_palette(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
# Four colors: white, grey, black, red
|
||||
# Four colors: white, gray, black, red
|
||||
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
|
||||
|
||||
im_list = []
|
||||
|
@ -1180,18 +1180,17 @@ def test_palette_save_L(tmp_path):
|
|||
|
||||
|
||||
def test_palette_save_P(tmp_path):
|
||||
# Pass in a different palette, then construct what the image would look like.
|
||||
# Forcing a non-straight grayscale palette.
|
||||
|
||||
im = hopper("P")
|
||||
palette = bytes(255 - i // 3 for i in range(768))
|
||||
im = Image.new("P", (1, 2))
|
||||
im.putpixel((0, 1), 1)
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im.save(out, palette=palette)
|
||||
im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
im.putpalette(palette)
|
||||
assert_image_equal(reloaded, im)
|
||||
reloaded_rgb = reloaded.convert("RGB")
|
||||
|
||||
assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
|
||||
assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
|
||||
|
||||
|
||||
def test_palette_save_duplicate_entries(tmp_path):
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import sys
|
||||
from io import StringIO
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, IptcImagePlugin
|
||||
|
||||
|
@ -30,6 +32,36 @@ def test_getiptcinfo_jpg_found():
|
|||
assert iptc[(2, 101)] == b"Hungary"
|
||||
|
||||
|
||||
def test_getiptcinfo_fotostation():
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb") as fp:
|
||||
data = bytearray(fp.read())
|
||||
data[86] = 240
|
||||
f = BytesIO(data)
|
||||
with Image.open(f) as im:
|
||||
# Act
|
||||
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||
|
||||
# Assert
|
||||
for tag in iptc.keys():
|
||||
if tag[0] == 240:
|
||||
return
|
||||
pytest.fail("FotoStation tag not found")
|
||||
|
||||
|
||||
def test_getiptcinfo_zero_padding():
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.info["photoshop"][0x0404] += b"\x00\x00\x00"
|
||||
|
||||
# Act
|
||||
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||
|
||||
# Assert
|
||||
assert isinstance(iptc, dict)
|
||||
assert len(iptc) == 3
|
||||
|
||||
|
||||
def test_getiptcinfo_tiff_none():
|
||||
# Arrange
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
|
|
|
@ -643,6 +643,23 @@ class TestFileJpeg:
|
|||
assert max(im2.quantization[0]) <= 255
|
||||
assert max(im2.quantization[1]) <= 255
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"blocks, rows, markers",
|
||||
((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
|
||||
)
|
||||
def test_restart_markers(self, blocks, rows, markers):
|
||||
im = Image.new("RGB", (32, 32)) # 16 MCUs
|
||||
out = BytesIO()
|
||||
im.save(
|
||||
out,
|
||||
format="JPEG",
|
||||
restart_marker_blocks=blocks,
|
||||
restart_marker_rows=rows,
|
||||
# force 8x8 pixel MCUs
|
||||
subsampling=0,
|
||||
)
|
||||
assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers
|
||||
|
||||
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
||||
def test_load_djpeg(self):
|
||||
with Image.open(TEST_FILE) as img:
|
||||
|
@ -767,6 +784,13 @@ class TestFileJpeg:
|
|||
# This should return the default
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_dpi_exif_truncated(self):
|
||||
# Arrange
|
||||
with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
|
||||
# Act / Assert
|
||||
# This should return the default
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_no_dpi_in_exif(self):
|
||||
# Arrange
|
||||
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
|
||||
|
@ -882,7 +906,10 @@ class TestFileJpeg:
|
|||
def test_getxmp(self):
|
||||
with Image.open("Tests/images/xmp_test.jpg") as im:
|
||||
if ElementTree is None:
|
||||
with pytest.warns(UserWarning):
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
xmp = im.getxmp()
|
||||
|
@ -905,6 +932,28 @@ class TestFileJpeg:
|
|||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
assert im.getxmp() == {}
|
||||
|
||||
def test_getxmp_no_prefix(self):
|
||||
with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
|
||||
if ElementTree is None:
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert im.getxmp() == {"xmpmeta": {"key": "value"}}
|
||||
|
||||
def test_getxmp_padded(self):
|
||||
with Image.open("Tests/images/xmp_padded.jpg") as im:
|
||||
if ElementTree is None:
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert im.getxmp() == {"xmpmeta": None}
|
||||
|
||||
@pytest.mark.timeout(timeout=1)
|
||||
def test_eof(self):
|
||||
# Even though this decoder never says that it is finished
|
||||
|
@ -929,6 +978,28 @@ class TestFileJpeg:
|
|||
im.load()
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
def test_separate_tables(self):
|
||||
im = hopper()
|
||||
data = [] # [interchange, tables-only, image-only]
|
||||
for streamtype in range(3):
|
||||
out = BytesIO()
|
||||
im.save(out, format="JPEG", streamtype=streamtype)
|
||||
data.append(out.getvalue())
|
||||
|
||||
# SOI, EOI
|
||||
for marker in b"\xff\xd8", b"\xff\xd9":
|
||||
assert marker in data[1] and marker in data[2]
|
||||
# DHT, DQT
|
||||
for marker in b"\xff\xc4", b"\xff\xdb":
|
||||
assert marker in data[1] and marker not in data[2]
|
||||
# SOF0, SOS, APP0 (JFIF header)
|
||||
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
|
||||
assert marker not in data[1] and marker in data[2]
|
||||
|
||||
with Image.open(BytesIO(data[0])) as interchange_im:
|
||||
with Image.open(BytesIO(data[1] + data[2])) as combined_im:
|
||||
assert_image_equal(interchange_im, combined_im)
|
||||
|
||||
def test_repr_jpeg(self):
|
||||
im = hopper()
|
||||
|
||||
|
|
|
@ -416,7 +416,7 @@ def test_plt_marker():
|
|||
while True:
|
||||
marker = out.read(2)
|
||||
if not marker:
|
||||
assert False, "End of stream without PLT"
|
||||
pytest.fail("End of stream without PLT")
|
||||
|
||||
jp2_boxid = _binary.i16be(marker)
|
||||
if jp2_boxid == 0xFF4F:
|
||||
|
@ -426,7 +426,7 @@ def test_plt_marker():
|
|||
# PLT
|
||||
return
|
||||
elif jp2_boxid == 0xFF93:
|
||||
assert False, "SOD without finding PLT first"
|
||||
pytest.fail("SOD without finding PLT first")
|
||||
|
||||
hdr = out.read(2)
|
||||
length = _binary.i16be(hdr)
|
||||
|
|
|
@ -8,7 +8,7 @@ from collections import namedtuple
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features
|
||||
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
|
||||
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
|
||||
from .helper import (
|
||||
|
@ -1035,7 +1035,18 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||
for i in range(2, 9):
|
||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||
assert 274 in im.tag_v2
|
||||
|
||||
im.load()
|
||||
assert 274 not in im.tag_v2
|
||||
|
||||
assert_image_similar(base_im, im, 0.7)
|
||||
|
||||
def test_exif_transpose(self):
|
||||
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||
for i in range(2, 9):
|
||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||
im = ImageOps.exif_transpose(im)
|
||||
|
||||
assert_image_similar(base_im, im, 0.7)
|
||||
|
||||
|
|
|
@ -26,8 +26,7 @@ def open_with_magick(magick, tmp_path, f):
|
|||
rc = subprocess.call(
|
||||
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
||||
)
|
||||
if rc:
|
||||
raise OSError
|
||||
assert not rc
|
||||
return Image.open(outfile)
|
||||
|
||||
|
||||
|
|
|
@ -92,11 +92,11 @@ class TestFilePng:
|
|||
assert im.format == "PNG"
|
||||
assert im.get_format_mimetype() == "image/png"
|
||||
|
||||
for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
|
||||
for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
|
||||
im = hopper(mode)
|
||||
im.save(test_file)
|
||||
with Image.open(test_file) as reloaded:
|
||||
if mode == "I;16":
|
||||
if mode in ("I;16", "I;16B"):
|
||||
reloaded = reloaded.convert(mode)
|
||||
assert_image_equal(reloaded, im)
|
||||
|
||||
|
@ -297,7 +297,7 @@ class TestFilePng:
|
|||
assert_image(im, "RGBA", (10, 10))
|
||||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||
|
||||
def test_save_greyscale_transparency(self, tmp_path):
|
||||
def test_save_grayscale_transparency(self, tmp_path):
|
||||
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
|
||||
in_file = "Tests/images/" + mode.lower() + "_trns.png"
|
||||
with Image.open(in_file) as im:
|
||||
|
@ -665,7 +665,10 @@ class TestFilePng:
|
|||
def test_getxmp(self):
|
||||
with Image.open("Tests/images/color_snakes.png") as im:
|
||||
if ElementTree is None:
|
||||
with pytest.warns(UserWarning):
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
xmp = im.getxmp()
|
||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
|||
|
||||
from PIL import Image, QoiImagePlugin
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar_tofile
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
||||
|
||||
def test_sanity():
|
||||
|
@ -18,7 +18,7 @@ def test_sanity():
|
|||
assert im.size == (162, 150)
|
||||
assert im.format == "QOI"
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03)
|
||||
assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
|
|
|
@ -734,7 +734,10 @@ class TestFileTiff:
|
|||
def test_getxmp(self):
|
||||
with Image.open("Tests/images/lab.tif") as im:
|
||||
if ElementTree is None:
|
||||
with pytest.warns(UserWarning):
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
xmp = im.getxmp()
|
||||
|
|
|
@ -233,4 +233,15 @@ class TestFileWebp:
|
|||
im.save(out_webp, save_all=True)
|
||||
|
||||
with Image.open(out_webp) as reloaded:
|
||||
reloaded.load()
|
||||
assert reloaded.info["duration"] == 1000
|
||||
|
||||
def test_roundtrip_rgba_palette(self, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = Image.new("RGBA", (1, 1)).convert("P")
|
||||
assert im.mode == "P"
|
||||
assert im.palette.mode == "RGBA"
|
||||
im.save(temp_file)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
|
|
|
@ -118,7 +118,10 @@ def test_getxmp():
|
|||
|
||||
with Image.open("Tests/images/flower2.webp") as im:
|
||||
if ElementTree is None:
|
||||
with pytest.warns(UserWarning):
|
||||
with pytest.warns(
|
||||
UserWarning,
|
||||
match="XMP data cannot be read without defusedxml dependency",
|
||||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import io
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
@ -638,8 +639,8 @@ class TestImage:
|
|||
im.remap_palette(None)
|
||||
|
||||
def test_remap_palette_transparency(self):
|
||||
im = Image.new("P", (1, 2))
|
||||
im.putpixel((0, 1), 1)
|
||||
im = Image.new("P", (1, 2), (0, 0, 0))
|
||||
im.putpixel((0, 1), (255, 0, 0))
|
||||
im.info["transparency"] = 0
|
||||
|
||||
im_remapped = im.remap_palette([1, 0])
|
||||
|
@ -906,6 +907,38 @@ class TestImage:
|
|||
im = Image.new("RGB", size)
|
||||
assert im.tobytes() == b""
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_frombytes(self, size):
|
||||
Image.frombytes("RGB", size, b"")
|
||||
|
||||
im = Image.new("RGB", size)
|
||||
im.frombytes(b"")
|
||||
|
||||
def test_has_transparency_data(self):
|
||||
for mode in ("1", "L", "P", "RGB"):
|
||||
im = Image.new(mode, (1, 1))
|
||||
assert not im.has_transparency_data
|
||||
|
||||
for mode in ("LA", "La", "PA", "RGBA", "RGBa"):
|
||||
im = Image.new(mode, (1, 1))
|
||||
assert im.has_transparency_data
|
||||
|
||||
# P mode with "transparency" info
|
||||
with Image.open("Tests/images/first_frame_transparency.gif") as im:
|
||||
assert "transparency" in im.info
|
||||
assert im.has_transparency_data
|
||||
|
||||
# RGB mode with "transparency" info
|
||||
with Image.open("Tests/images/rgb_trns.png") as im:
|
||||
assert "transparency" in im.info
|
||||
assert im.has_transparency_data
|
||||
|
||||
# P mode with RGBA palette
|
||||
im = Image.new("RGBA", (1, 1)).convert("P")
|
||||
assert im.mode == "P"
|
||||
assert im.palette.mode == "RGBA"
|
||||
assert im.has_transparency_data
|
||||
|
||||
def test_apply_transparency(self):
|
||||
im = Image.new("P", (1, 1))
|
||||
im.putpalette((0, 0, 0, 1, 1, 1))
|
||||
|
@ -967,7 +1000,7 @@ class TestImage:
|
|||
with Image.open(os.path.join("Tests/images", path)) as im:
|
||||
try:
|
||||
im.load()
|
||||
assert False
|
||||
pytest.fail()
|
||||
except OSError as e:
|
||||
buffer_overrun = str(e) == "buffer overrun when reading image file"
|
||||
truncated = "image file is truncated" in str(e)
|
||||
|
@ -978,10 +1011,19 @@ class TestImage:
|
|||
with Image.open("Tests/images/fli_overrun2.bin") as im:
|
||||
try:
|
||||
im.seek(1)
|
||||
assert False
|
||||
pytest.fail()
|
||||
except OSError as e:
|
||||
assert str(e) == "buffer overrun when reading image file"
|
||||
|
||||
def test_close_graceful(self, caplog):
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
copy = im.copy()
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
im.close()
|
||||
copy.close()
|
||||
assert len(caplog.records) == 0
|
||||
assert im.fp is None
|
||||
|
||||
|
||||
class MockEncoder:
|
||||
pass
|
||||
|
|
|
@ -130,9 +130,16 @@ class TestImageGetPixel(AccessTest):
|
|||
bands = Image.getmodebands(mode)
|
||||
if bands == 1:
|
||||
return 1
|
||||
if mode in ("BGR;15", "BGR;16"):
|
||||
# These modes have less than 8 bits per band
|
||||
# So (1, 2, 3) cannot be roundtripped
|
||||
return (16, 32, 49)
|
||||
return tuple(range(1, bands + 1))
|
||||
|
||||
def check(self, mode, expected_color=None):
|
||||
if self._need_cffi_access and mode.startswith("BGR;"):
|
||||
pytest.skip("Support not added to deprecated module for BGR;* modes")
|
||||
|
||||
if not expected_color:
|
||||
expected_color = self.color(mode)
|
||||
|
||||
|
@ -203,6 +210,9 @@ class TestImageGetPixel(AccessTest):
|
|||
"F",
|
||||
"P",
|
||||
"PA",
|
||||
"BGR;15",
|
||||
"BGR;16",
|
||||
"BGR;24",
|
||||
"RGB",
|
||||
"RGBA",
|
||||
"RGBX",
|
||||
|
|
|
@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
|
|||
f = str(tmp_path / "temp.png")
|
||||
|
||||
im_l = im.convert("L")
|
||||
assert im_l.info["transparency"] == 1 # undone
|
||||
assert im_l.info["transparency"] == 0
|
||||
im_l.save(f)
|
||||
|
||||
im_rgb = im.convert("RGB")
|
||||
assert im_rgb.info["transparency"] == (0, 1, 2) # undone
|
||||
assert im_rgb.info["transparency"] == (0, 0, 0)
|
||||
im_rgb.save(f)
|
||||
|
||||
|
||||
|
|
|
@ -76,6 +76,15 @@ def test_mode_F():
|
|||
assert list(im.getdata()) == target
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
|
||||
def test_mode_BGR(mode):
|
||||
data = [(16, 32, 49), (32, 32, 98)]
|
||||
im = Image.new(mode, (1, 2))
|
||||
im.putdata(data)
|
||||
|
||||
assert list(im.getdata()) == data
|
||||
|
||||
|
||||
def test_array_B():
|
||||
# shouldn't segfault
|
||||
# see https://github.com/python-pillow/Pillow/issues/1008
|
||||
|
|
|
@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
|
|||
im.putpalette(palette, mode)
|
||||
assert im.getpalette() == [1, 2, 3]
|
||||
assert im.palette.colors == {(1, 2, 3, 4): 0}
|
||||
|
||||
|
||||
def test_empty_palette():
|
||||
im = Image.new("P", (1, 1))
|
||||
assert im.getpalette() == []
|
||||
|
||||
|
||||
def test_undefined_palette_index():
|
||||
im = Image.new("P", (1, 1), 3)
|
||||
im.putpalette((1, 2, 3))
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)
|
||||
|
|
|
@ -67,7 +67,7 @@ def test_quantize_no_dither():
|
|||
|
||||
def test_quantize_no_dither2():
|
||||
im = Image.new("RGB", (9, 1))
|
||||
im.putdata(list((p,) * 3 for p in range(0, 36, 4)))
|
||||
im.putdata([(p,) * 3 for p in range(0, 36, 4)])
|
||||
|
||||
palette = Image.new("P", (1, 1))
|
||||
data = (0, 0, 0, 32, 32, 32)
|
||||
|
|
|
@ -195,7 +195,7 @@ class TestReducingGapResize:
|
|||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
|
||||
)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
assert_image_equal(ref, im)
|
||||
|
||||
assert_image_similar(ref, im, epsilon)
|
||||
|
@ -210,7 +210,7 @@ class TestReducingGapResize:
|
|||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
|
||||
)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
assert_image_equal(ref, im)
|
||||
|
||||
assert_image_similar(ref, im, epsilon)
|
||||
|
@ -225,7 +225,7 @@ class TestReducingGapResize:
|
|||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
|
||||
)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
assert_image_equal(ref, im)
|
||||
|
||||
assert_image_similar(ref, im, epsilon)
|
||||
|
|
|
@ -147,7 +147,7 @@ def test_reducing_gap_values():
|
|||
|
||||
ref = hopper()
|
||||
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
assert_image_equal(ref, im)
|
||||
|
||||
assert_image_similar(ref, im, 3.5)
|
||||
|
|
|
@ -10,7 +10,7 @@ GREEN = (0, 255, 0)
|
|||
ORANGE = (255, 128, 0)
|
||||
WHITE = (255, 255, 255)
|
||||
|
||||
GREY = 128
|
||||
GRAY = 128
|
||||
|
||||
|
||||
def test_sanity():
|
||||
|
@ -121,12 +121,12 @@ def test_constant():
|
|||
im = Image.new("RGB", (20, 10))
|
||||
|
||||
# Act
|
||||
new = ImageChops.constant(im, GREY)
|
||||
new = ImageChops.constant(im, GRAY)
|
||||
|
||||
# Assert
|
||||
assert new.size == im.size
|
||||
assert new.getpixel((0, 0)) == GREY
|
||||
assert new.getpixel((19, 9)) == GREY
|
||||
assert new.getpixel((0, 0)) == GRAY
|
||||
assert new.getpixel((19, 9)) == GRAY
|
||||
|
||||
|
||||
def test_darker_image():
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import contextlib
|
||||
import os.path
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageColor, ImageDraw, ImageFont
|
||||
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -586,6 +587,18 @@ def test_point(points):
|
|||
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
|
||||
|
||||
|
||||
def test_point_I16():
|
||||
# Arrange
|
||||
im = Image.new("I;16", (1, 1))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
draw.point((0, 0), fill=0x1234)
|
||||
|
||||
# Assert
|
||||
assert im.getpixel((0, 0)) == 0x1234
|
||||
|
||||
|
||||
@pytest.mark.parametrize("points", POINTS)
|
||||
def test_polygon(points):
|
||||
# Arrange
|
||||
|
@ -732,7 +745,7 @@ def test_rectangle_I16(bbox):
|
|||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
draw.rectangle(bbox, fill="black", outline="green")
|
||||
draw.rectangle(bbox, outline=0xFFFF)
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
|
||||
|
@ -1341,7 +1354,33 @@ def test_setting_default_font():
|
|||
assert draw.getfont() == font
|
||||
finally:
|
||||
ImageDraw.ImageDraw.font = None
|
||||
assert isinstance(draw.getfont(), ImageFont.ImageFont)
|
||||
assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
|
||||
|
||||
|
||||
def test_default_font_size():
|
||||
freetype_support = features.check_module("freetype2")
|
||||
text = "Default font at a specific size."
|
||||
|
||||
im = Image.new("RGB", (220, 25))
|
||||
draw = ImageDraw.Draw(im)
|
||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
||||
draw.text((0, 0), text, font_size=16)
|
||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
||||
|
||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
||||
assert draw.textlength(text, font_size=16) == 216
|
||||
|
||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
||||
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
|
||||
|
||||
im = Image.new("RGB", (220, 25))
|
||||
draw = ImageDraw.Draw(im)
|
||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
||||
draw.multiline_text((0, 0), text, font_size=16)
|
||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
||||
|
||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
||||
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
|
|
|
@ -4,6 +4,7 @@ import re
|
|||
import shutil
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
@ -76,8 +77,9 @@ def _render(font, layout_engine):
|
|||
return img
|
||||
|
||||
|
||||
def test_font_with_name(layout_engine):
|
||||
_render(FONT_PATH, layout_engine)
|
||||
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
|
||||
def test_font_with_name(layout_engine, font):
|
||||
_render(font, layout_engine)
|
||||
|
||||
|
||||
def test_font_with_filelike(layout_engine):
|
||||
|
@ -141,7 +143,9 @@ def test_I16(font):
|
|||
draw = ImageDraw.Draw(im)
|
||||
|
||||
txt = "Hello World!"
|
||||
draw.text((10, 10), txt, font=font)
|
||||
draw.text((10, 10), txt, fill=0xFFFE, font=font)
|
||||
|
||||
assert im.getpixel((12, 14)) == 0xFFFE
|
||||
|
||||
target = "Tests/images/transparent_background_text_L.png"
|
||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||
|
@ -301,8 +305,8 @@ def test_multiline_spacing(font):
|
|||
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
||||
)
|
||||
def test_rotated_transposed_font(font, orientation):
|
||||
img_grey = Image.new("L", (100, 100))
|
||||
draw = ImageDraw.Draw(img_grey)
|
||||
img_gray = Image.new("L", (100, 100))
|
||||
draw = ImageDraw.Draw(img_gray)
|
||||
word = "testing"
|
||||
|
||||
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
||||
|
@ -342,8 +346,8 @@ def test_rotated_transposed_font(font, orientation):
|
|||
),
|
||||
)
|
||||
def test_unrotated_transposed_font(font, orientation):
|
||||
img_grey = Image.new("L", (100, 100))
|
||||
draw = ImageDraw.Draw(img_grey)
|
||||
img_gray = Image.new("L", (100, 100))
|
||||
draw = ImageDraw.Draw(img_gray)
|
||||
word = "testing"
|
||||
|
||||
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
||||
|
@ -451,7 +455,7 @@ def test_load_non_font_bytes():
|
|||
|
||||
def test_default_font():
|
||||
# Arrange
|
||||
txt = 'This is a "better than nothing" default font.'
|
||||
txt = "This is a default font using FreeType support."
|
||||
im = Image.new(mode="RGB", size=(300, 100))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
|
@ -459,8 +463,11 @@ def test_default_font():
|
|||
default_font = ImageFont.load_default()
|
||||
draw.text((10, 10), txt, font=default_font)
|
||||
|
||||
larger_default_font = ImageFont.load_default(size=14)
|
||||
draw.text((10, 60), txt, font=larger_default_font)
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im, "Tests/images/default_font.png")
|
||||
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
||||
|
@ -483,14 +490,6 @@ def test_render_empty(font):
|
|||
assert_image_equal(im, target)
|
||||
|
||||
|
||||
def test_unicode_pilfont():
|
||||
# should not segfault, should return UnicodeDecodeError
|
||||
# issue #2826
|
||||
font = ImageFont.load_default()
|
||||
with pytest.raises(UnicodeEncodeError):
|
||||
font.getbbox("’")
|
||||
|
||||
|
||||
def test_unicode_extended(layout_engine):
|
||||
# issue #3777
|
||||
text = "A\u278A\U0001F12B"
|
||||
|
@ -720,14 +719,6 @@ def test_variation_set_by_axes(font):
|
|||
_check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
|
||||
|
||||
|
||||
def test_textbbox_non_freetypefont():
|
||||
im = Image.new("RGB", (200, 200))
|
||||
d = ImageDraw.Draw(im)
|
||||
default_font = ImageFont.load_default()
|
||||
assert d.textlength("test", font=default_font) == 24
|
||||
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"anchor, left, top",
|
||||
(
|
||||
|
@ -1082,3 +1073,9 @@ def test_raqm_missing_warning(monkeypatch):
|
|||
"Raqm layout was requested, but Raqm is not available. "
|
||||
"Falling back to basic layout."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [-1, 0])
|
||||
def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size):
|
||||
with pytest.raises(ValueError):
|
||||
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
|
||||
|
|
45
Tests/test_imagefontpil.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, features
|
||||
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
features.check_module("freetype2"),
|
||||
reason="PILfont superseded if FreeType is supported",
|
||||
)
|
||||
|
||||
|
||||
def test_default_font():
|
||||
# Arrange
|
||||
txt = 'This is a "better than nothing" default font.'
|
||||
im = Image.new(mode="RGB", size=(300, 100))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
default_font = ImageFont.load_default()
|
||||
draw.text((10, 10), txt, font=default_font)
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im, "Tests/images/default_font.png")
|
||||
|
||||
|
||||
def test_size_without_freetype():
|
||||
with pytest.raises(ImportError):
|
||||
ImageFont.load_default(size=14)
|
||||
|
||||
|
||||
def test_unicode():
|
||||
# should not segfault, should return UnicodeDecodeError
|
||||
# issue #2826
|
||||
font = ImageFont.load_default()
|
||||
with pytest.raises(UnicodeEncodeError):
|
||||
font.getbbox("’")
|
||||
|
||||
|
||||
def test_textbbox():
|
||||
im = Image.new("RGB", (200, 200))
|
||||
d = ImageDraw.Draw(im)
|
||||
default_font = ImageFont.load_default()
|
||||
assert d.textlength("test", font=default_font) == 24
|
||||
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
|
|
@ -11,6 +11,10 @@ from .helper import assert_image_equal_tofile, skip_unless_feature
|
|||
|
||||
|
||||
class TestImageGrab:
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("USERNAME") == "ContainerAdministrator",
|
||||
reason="can't grab screen when running in Docker",
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
|
||||
)
|
||||
|
|
|
@ -39,6 +39,9 @@ def test_sanity():
|
|||
ImageOps.contain(hopper("L"), (128, 128))
|
||||
ImageOps.contain(hopper("RGB"), (128, 128))
|
||||
|
||||
ImageOps.cover(hopper("L"), (128, 128))
|
||||
ImageOps.cover(hopper("RGB"), (128, 128))
|
||||
|
||||
ImageOps.crop(hopper("L"), 1)
|
||||
ImageOps.crop(hopper("RGB"), 1)
|
||||
|
||||
|
@ -119,6 +122,20 @@ def test_contain_round():
|
|||
assert new_im.height == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"image_name, expected_size",
|
||||
(
|
||||
("colr_bungee.png", (1024, 256)), # landscape
|
||||
("imagedraw_stroke_multiline.png", (256, 640)), # portrait
|
||||
("hopper.png", (256, 256)), # square
|
||||
),
|
||||
)
|
||||
def test_cover(image_name, expected_size):
|
||||
with Image.open("Tests/images/" + image_name) as im:
|
||||
new_im = ImageOps.cover(im, (256, 256))
|
||||
assert new_im.size == expected_size
|
||||
|
||||
|
||||
def test_pad():
|
||||
# Same ratio
|
||||
im = hopper()
|
||||
|
@ -416,6 +433,12 @@ def test_exif_transpose_in_place():
|
|||
assert_image_equal(im, expected)
|
||||
|
||||
|
||||
def test_autocontrast_unsupported_mode():
|
||||
im = Image.new("RGBA", (1, 1))
|
||||
with pytest.raises(OSError):
|
||||
ImageOps.autocontrast(im)
|
||||
|
||||
|
||||
def test_autocontrast_cutoff():
|
||||
# Test the cutoff argument of autocontrast
|
||||
with Image.open("Tests/images/bw_gradient.png") as img:
|
||||
|
|
|
@ -85,7 +85,7 @@ def test_ipythonviewer():
|
|||
test_viewer = viewer
|
||||
break
|
||||
else:
|
||||
assert False
|
||||
pytest.fail()
|
||||
|
||||
im = hopper()
|
||||
assert test_viewer.show(im) == 1
|
||||
|
|
|
@ -340,6 +340,17 @@ class TestLibUnpack:
|
|||
self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
|
||||
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
|
||||
|
||||
self.assert_unpack(
|
||||
"RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
|
||||
)
|
||||
|
||||
def test_BGR(self):
|
||||
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
|
||||
self.assert_unpack(
|
||||
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
|
||||
)
|
||||
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
|
||||
|
||||
def test_RGBA(self):
|
||||
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
|
||||
self.assert_unpack(
|
||||
|
|
2
_custom_build/backend.py
Executable file → Normal file
|
@ -1,6 +1,6 @@
|
|||
import sys
|
||||
|
||||
from setuptools.build_meta import * # noqa: F401, F403
|
||||
from setuptools.build_meta import * # noqa: F403
|
||||
from setuptools.build_meta import build_wheel
|
||||
|
||||
backend_class = build_wheel.__self__.__class__
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install libimagequant
|
||||
|
||||
archive=libimagequant-4.2.0
|
||||
archive=libimagequant-4.2.2
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -11,4 +11,3 @@ pushd $archive
|
|||
meson build --prefix=/usr && sudo ninja -C build install
|
||||
|
||||
popd
|
||||
|
||||
|
|
|
@ -15,4 +15,3 @@ make && sudo make install
|
|||
cd ..
|
||||
|
||||
popd
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install webp
|
||||
|
||||
archive=libwebp-1.3.1
|
||||
archive=libwebp-1.3.2
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -2,4 +2,3 @@
|
|||
|
||||
pkg install -y python ndk-sysroot clang make \
|
||||
libjpeg-turbo
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ The fork author's goal is to foster and support active development of PIL throug
|
|||
|
||||
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
|
||||
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
|
||||
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels
|
||||
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
|
||||
.. _GitHub: https://github.com/python-pillow/Pillow
|
||||
.. _Python Package Index: https://pypi.org/project/Pillow/
|
||||
|
||||
|
|
25
docs/conf.py
|
@ -166,6 +166,12 @@ html_static_path = ["resources"]
|
|||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
html_css_files = ["css/dark.css"]
|
||||
|
||||
html_js_files = [
|
||||
"js/activate_tab.js",
|
||||
]
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
@ -313,19 +319,15 @@ texinfo_documents = [
|
|||
# texinfo_no_detailmenu = False
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_css_file("css/dark.css")
|
||||
|
||||
|
||||
linkcheck_allowed_redirects = {
|
||||
r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501
|
||||
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
|
||||
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
|
||||
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501
|
||||
r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
|
||||
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",
|
||||
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
|
||||
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
|
||||
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
|
||||
r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501
|
||||
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501
|
||||
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501
|
||||
r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
|
||||
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
|
||||
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
|
||||
}
|
||||
|
||||
# sphinx.ext.extlinks
|
||||
|
@ -338,6 +340,7 @@ extlinks = {
|
|||
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
|
||||
"issue": (_repo + "issues/%s", "#%s"),
|
||||
"pr": (_repo + "pull/%s", "#%s"),
|
||||
"pypi": ("https://pypi.org/project/%s/", "%s"),
|
||||
}
|
||||
|
||||
# sphinxext.opengraph
|
||||
|
|
|
@ -10,7 +10,7 @@ Deprecated features
|
|||
-------------------
|
||||
|
||||
Below are features which are considered deprecated. Where appropriate,
|
||||
a ``DeprecationWarning`` is issued.
|
||||
a :py:exc:`DeprecationWarning` is issued.
|
||||
|
||||
PSFile
|
||||
~~~~~~
|
||||
|
@ -267,7 +267,7 @@ ImageFile.raise_ioerror
|
|||
.. deprecated:: 7.2.0
|
||||
.. versionremoved:: 9.0.0
|
||||
|
||||
``IOError`` was merged into ``OSError`` in Python 3.3.
|
||||
:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3.
|
||||
So, ``ImageFile.raise_ioerror`` has been removed.
|
||||
Use ``ImageFile.raise_oserror`` instead.
|
||||
|
||||
|
@ -293,9 +293,9 @@ im.offset
|
|||
``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
|
||||
|
||||
It was documented as deprecated in PIL 1.1.2,
|
||||
raised a ``DeprecationWarning`` since 1.1.5,
|
||||
an ``Exception`` since Pillow 3.0.0
|
||||
and ``NotImplementedError`` since 3.3.0.
|
||||
raised a :py:exc:`DeprecationWarning` since 1.1.5,
|
||||
an :py:exc:`Exception` since Pillow 3.0.0
|
||||
and :py:exc:`NotImplementedError` since 3.3.0.
|
||||
|
||||
Image.fromstring, im.fromstring and im.tostring
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -307,9 +307,9 @@ Image.fromstring, im.fromstring and im.tostring
|
|||
* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
|
||||
* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
|
||||
|
||||
They issued a ``DeprecationWarning`` since 2.0.0,
|
||||
an ``Exception`` since 3.0.0
|
||||
and ``NotImplementedError`` since 3.3.0.
|
||||
They issued a :py:exc:`DeprecationWarning` since 2.0.0,
|
||||
an :py:exc:`Exception` since 3.0.0
|
||||
and :py:exc:`NotImplementedError` since 3.3.0.
|
||||
|
||||
ImageCms.CmsProfile attributes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -318,7 +318,7 @@ ImageCms.CmsProfile attributes
|
|||
.. versionremoved:: 8.0.0
|
||||
|
||||
Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
|
||||
they issued a ``DeprecationWarning``:
|
||||
they issued a :py:exc:`DeprecationWarning`:
|
||||
|
||||
======================== ===================================================
|
||||
Removed Use instead
|
||||
|
@ -442,7 +442,7 @@ PIL.OleFileIO
|
|||
.. deprecated:: 4.0.0
|
||||
.. versionremoved:: 6.0.0
|
||||
|
||||
PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
|
||||
the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0
|
||||
``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
|
||||
the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
|
||||
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
|
||||
PyPI (eg. ``python3 -m pip install olefile``).
|
||||
|
|
BIN
docs/example/image_thumbnail.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_contain.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_cover.png
Normal file
After Width: | Height: | Size: 38 KiB |