mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-09-24 13:07:00 +03:00
Merge branch 'main' into convert_mode
This commit is contained in:
commit
fc1680103d
|
@ -13,24 +13,21 @@ aptget_update()
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
|
||||||
aptget_update || aptget_update retry || aptget_update retry
|
aptget_update || aptget_update retry || aptget_update retry
|
||||||
fi
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
|
||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||||
sway wl-clipboard libopenblas-dev nasm
|
sway wl-clipboard libopenblas-dev nasm
|
||||||
fi
|
|
||||||
|
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
python3 -m pip install --upgrade wheel
|
python3 -m pip install --upgrade wheel
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
python3 -m pip install defusedxml
|
python3 -m pip install defusedxml
|
||||||
python3 -m pip install ipython
|
python3 -m pip install ipython
|
||||||
|
python3 -m pip install numpy
|
||||||
python3 -m pip install olefile
|
python3 -m pip install olefile
|
||||||
python3 -m pip install -U pytest
|
python3 -m pip install -U pytest
|
||||||
python3 -m pip install -U pytest-cov
|
python3 -m pip install -U pytest-cov
|
||||||
|
@ -40,9 +37,6 @@ python3 -m pip install pyroma
|
||||||
# fails on beta 3.14 and PyPy
|
# fails on beta 3.14 and PyPy
|
||||||
python3 -m pip install --only-binary=:all: pyarrow || true
|
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
|
||||||
python3 -m pip install numpy
|
|
||||||
|
|
||||||
# PyQt6 doesn't support PyPy3
|
# PyQt6 doesn't support PyPy3
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
||||||
|
@ -50,12 +44,6 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
|
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Pyroma uses non-isolated build and fails with old setuptools
|
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
|
|
||||||
# To match pyproject.toml
|
|
||||||
python3 -m pip install "setuptools>=77"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# webp
|
# webp
|
||||||
pushd depends && ./install_webp.sh && popd
|
pushd depends && ./install_webp.sh && popd
|
||||||
|
|
||||||
|
@ -70,6 +58,3 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
else
|
|
||||||
cd depends && ./install_extra_test_images.sh && cd ..
|
|
||||||
fi
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==3.0.0
|
cibuildwheel==3.1.4
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
mypy==1.16.1
|
mypy==1.17.1
|
||||||
IceSpringPySideStubs-PyQt6
|
IceSpringPySideStubs-PyQt6
|
||||||
IceSpringPySideStubs-PySide6
|
IceSpringPySideStubs-PySide6
|
||||||
ipython
|
ipython
|
||||||
numpy
|
numpy
|
||||||
packaging
|
packaging
|
||||||
pyarrow-stubs
|
pyarrow-stubs
|
||||||
|
pybind11
|
||||||
pytest
|
pytest
|
||||||
sphinx
|
sphinx
|
||||||
types-atheris
|
types-atheris
|
||||||
|
|
1
.github/mergify.yml
vendored
1
.github/mergify.yml
vendored
|
@ -8,7 +8,6 @@ pull_request_rules:
|
||||||
- status-success=Docker Test Successful
|
- status-success=Docker Test Successful
|
||||||
- status-success=Windows Test Successful
|
- status-success=Windows Test Successful
|
||||||
- status-success=MinGW
|
- status-success=MinGW
|
||||||
- status-success=Cygwin Test Successful
|
|
||||||
actions:
|
actions:
|
||||||
merge:
|
merge:
|
||||||
method: merge
|
method: merge
|
||||||
|
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
name: Docs
|
name: Docs
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
154
.github/workflows/test-cygwin.yml
vendored
154
.github/workflows/test-cygwin.yml
vendored
|
@ -1,154 +0,0 @@
|
||||||
name: Test Cygwin
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
paths-ignore:
|
|
||||||
- ".github/workflows/docs.yml"
|
|
||||||
- ".github/workflows/wheels*"
|
|
||||||
- ".gitmodules"
|
|
||||||
- "docs/**"
|
|
||||||
- "wheels/**"
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- ".github/workflows/docs.yml"
|
|
||||||
- ".github/workflows/wheels*"
|
|
||||||
- ".gitmodules"
|
|
||||||
- "docs/**"
|
|
||||||
- "wheels/**"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
COVERAGE_CORE: sysmon
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-minor-version: [9]
|
|
||||||
|
|
||||||
timeout-minutes: 40
|
|
||||||
|
|
||||||
name: Python 3.${{ matrix.python-minor-version }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Fix line endings
|
|
||||||
run: |
|
|
||||||
git config --global core.autocrlf input
|
|
||||||
|
|
||||||
- name: Checkout Pillow
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Cygwin
|
|
||||||
uses: cygwin/cygwin-install-action@v5
|
|
||||||
with:
|
|
||||||
packages: >
|
|
||||||
gcc-g++
|
|
||||||
ghostscript
|
|
||||||
git
|
|
||||||
ImageMagick
|
|
||||||
jpeg
|
|
||||||
libfreetype-devel
|
|
||||||
libimagequant-devel
|
|
||||||
libjpeg-devel
|
|
||||||
liblapack-devel
|
|
||||||
liblcms2-devel
|
|
||||||
libopenjp2-devel
|
|
||||||
libraqm-devel
|
|
||||||
libtiff-devel
|
|
||||||
libwebp-devel
|
|
||||||
libxcb-devel
|
|
||||||
libxcb-xinerama0
|
|
||||||
make
|
|
||||||
netpbm
|
|
||||||
perl
|
|
||||||
python3${{ matrix.python-minor-version }}-cython
|
|
||||||
python3${{ matrix.python-minor-version }}-devel
|
|
||||||
python3${{ matrix.python-minor-version }}-ipython
|
|
||||||
python3${{ matrix.python-minor-version }}-numpy
|
|
||||||
python3${{ matrix.python-minor-version }}-sip
|
|
||||||
python3${{ matrix.python-minor-version }}-tkinter
|
|
||||||
wget
|
|
||||||
xorg-server-extra
|
|
||||||
zlib-devel
|
|
||||||
|
|
||||||
- name: Add Lapack to PATH
|
|
||||||
uses: egor-tensin/cleanup-path@v4
|
|
||||||
with:
|
|
||||||
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
|
||||||
|
|
||||||
- name: Select Python version
|
|
||||||
run: |
|
|
||||||
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
|
||||||
|
|
||||||
- name: pip cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
|
||||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
|
|
||||||
|
|
||||||
- name: Build system information
|
|
||||||
run: |
|
|
||||||
dash.exe -c "python3 .github/workflows/system-info.py"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bash.exe .ci/install.sh
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
|
||||||
run: |
|
|
||||||
.ci/build.sh
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: |
|
|
||||||
bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
|
|
||||||
|
|
||||||
- name: Prepare to upload errors
|
|
||||||
if: failure()
|
|
||||||
run: |
|
|
||||||
dash.exe -c "mkdir -p Tests/errors"
|
|
||||||
|
|
||||||
- name: Upload errors
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: errors
|
|
||||||
path: Tests/errors
|
|
||||||
|
|
||||||
- name: After success
|
|
||||||
run: |
|
|
||||||
bash.exe .ci/after_success.sh
|
|
||||||
rm C:\cygwin\bin\bash.EXE
|
|
||||||
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
with:
|
|
||||||
files: ./coverage.xml
|
|
||||||
flags: GHA_Cygwin
|
|
||||||
name: Cygwin Python 3.${{ matrix.python-minor-version }}
|
|
||||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
|
||||||
|
|
||||||
success:
|
|
||||||
permissions:
|
|
||||||
contents: none
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Cygwin Test Successful
|
|
||||||
steps:
|
|
||||||
- name: Success
|
|
||||||
run: echo Cygwin Test Successful
|
|
4
.github/workflows/test-docker.yml
vendored
4
.github/workflows/test-docker.yml
vendored
|
@ -47,6 +47,8 @@ jobs:
|
||||||
centos-stream-10-amd64,
|
centos-stream-10-amd64,
|
||||||
debian-12-bookworm-x86,
|
debian-12-bookworm-x86,
|
||||||
debian-12-bookworm-amd64,
|
debian-12-bookworm-amd64,
|
||||||
|
debian-13-trixie-x86,
|
||||||
|
debian-13-trixie-amd64,
|
||||||
fedora-41-amd64,
|
fedora-41-amd64,
|
||||||
fedora-42-amd64,
|
fedora-42-amd64,
|
||||||
gentoo,
|
gentoo,
|
||||||
|
@ -66,7 +68,7 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
2
.github/workflows/test-mingw.yml
vendored
2
.github/workflows/test-mingw.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
2
.github/workflows/test-valgrind-memory.yml
vendored
2
.github/workflows/test-valgrind-memory.yml
vendored
|
@ -41,7 +41,7 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
2
.github/workflows/test-valgrind.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|
10
.github/workflows/test-windows.yml
vendored
10
.github/workflows/test-windows.yml
vendored
|
@ -35,11 +35,11 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"]
|
python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"]
|
||||||
architecture: ["x64"]
|
architecture: ["x64"]
|
||||||
include:
|
include:
|
||||||
# Test the oldest Python on 32-bit
|
# Test the oldest Python on 32-bit
|
||||||
- { python-version: "3.9", architecture: "x86" }
|
- { python-version: "3.10", architecture: "x86" }
|
||||||
|
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
|
|
||||||
|
@ -47,19 +47,19 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Checkout cached dependencies
|
- name: Checkout cached dependencies
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: python-pillow/pillow-depends
|
repository: python-pillow/pillow-depends
|
||||||
path: winbuild\depends
|
path: winbuild\depends
|
||||||
|
|
||||||
- name: Checkout extra test images
|
- name: Checkout extra test images
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: python-pillow/test-images
|
repository: python-pillow/test-images
|
||||||
|
|
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
|
@ -42,7 +42,6 @@ jobs:
|
||||||
]
|
]
|
||||||
python-version: [
|
python-version: [
|
||||||
"pypy3.11",
|
"pypy3.11",
|
||||||
"pypy3.10",
|
|
||||||
"3.14t",
|
"3.14t",
|
||||||
"3.14",
|
"3.14",
|
||||||
"3.13t",
|
"3.13t",
|
||||||
|
@ -50,24 +49,23 @@ jobs:
|
||||||
"3.12",
|
"3.12",
|
||||||
"3.11",
|
"3.11",
|
||||||
"3.10",
|
"3.10",
|
||||||
"3.9",
|
|
||||||
]
|
]
|
||||||
include:
|
include:
|
||||||
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
- { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
||||||
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
|
- { python-version: "3.11", PYTHONOPTIMIZE: 2 }
|
||||||
# Free-threaded
|
# Free-threaded
|
||||||
- { python-version: "3.14t", disable-gil: true }
|
- { python-version: "3.14t", disable-gil: true }
|
||||||
- { python-version: "3.13t", disable-gil: true }
|
- { python-version: "3.13t", disable-gil: true }
|
||||||
# M1 only available for 3.10+
|
# Intel
|
||||||
- { os: "macos-13", python-version: "3.9" }
|
- { os: "macos-13", python-version: "3.10" }
|
||||||
exclude:
|
exclude:
|
||||||
- { os: "macos-latest", python-version: "3.9" }
|
- { os: "macos-latest", python-version: "3.10" }
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
@ -113,7 +111,7 @@ jobs:
|
||||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Register gcc problem matcher
|
- name: Register gcc problem matcher
|
||||||
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
|
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'"
|
||||||
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
|
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
81
.github/workflows/wheels-dependencies.sh
vendored
81
.github/workflows/wheels-dependencies.sh
vendored
|
@ -60,7 +60,7 @@ if [[ "$CIBW_PLATFORM" == "ios" ]]; then
|
||||||
# on using the Xcode builder, which isn't very helpful for most of Pillow's
|
# on using the Xcode builder, which isn't very helpful for most of Pillow's
|
||||||
# dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS
|
# dependencies. Therefore, we lean on the OSX configurations, plus CC, CFLAGS
|
||||||
# etc. to ensure the right sysroot is selected.
|
# etc. to ensure the right sysroot is selected.
|
||||||
HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO"
|
HOST_CMAKE_FLAGS="-DCMAKE_SYSTEM_NAME=$CMAKE_SYSTEM_NAME -DCMAKE_SYSTEM_PROCESSOR=$GNU_ARCH -DCMAKE_OSX_DEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET -DCMAKE_OSX_SYSROOT=$IOS_SDK_PATH -DBUILD_SHARED_LIBS=NO -DENABLE_SHARED=NO"
|
||||||
|
|
||||||
# Meson needs to be pointed at a cross-platform configuration file
|
# Meson needs to be pointed at a cross-platform configuration file
|
||||||
# This will be generated once CC etc. have been evaluated.
|
# This will be generated once CC etc. have been evaluated.
|
||||||
|
@ -94,16 +94,16 @@ ARCHIVE_SDIR=pillow-depends-main
|
||||||
# annotations have a source code patch that is required for some platforms. If
|
# annotations have a source code patch that is required for some platforms. If
|
||||||
# you change those versions, ensure the patch is also updated.
|
# you change those versions, ensure the patch is also updated.
|
||||||
FREETYPE_VERSION=2.13.3
|
FREETYPE_VERSION=2.13.3
|
||||||
HARFBUZZ_VERSION=11.2.1
|
HARFBUZZ_VERSION=11.3.3
|
||||||
LIBPNG_VERSION=1.6.49
|
LIBPNG_VERSION=1.6.50
|
||||||
JPEGTURBO_VERSION=3.1.1
|
JPEGTURBO_VERSION=3.1.2
|
||||||
OPENJPEG_VERSION=2.5.3
|
OPENJPEG_VERSION=2.5.3
|
||||||
XZ_VERSION=5.8.1
|
XZ_VERSION=5.8.1
|
||||||
|
ZSTD_VERSION=1.5.7
|
||||||
TIFF_VERSION=4.7.0
|
TIFF_VERSION=4.7.0
|
||||||
LCMS2_VERSION=2.17
|
LCMS2_VERSION=2.17
|
||||||
ZLIB_VERSION=1.3.1
|
ZLIB_NG_VERSION=2.2.5
|
||||||
ZLIB_NG_VERSION=2.2.4
|
LIBWEBP_VERSION=1.6.0
|
||||||
LIBWEBP_VERSION=1.5.0 # Patched; next release won't need patching. See patch file.
|
|
||||||
BZIP2_VERSION=1.0.8
|
BZIP2_VERSION=1.0.8
|
||||||
LIBXCB_VERSION=1.17.0
|
LIBXCB_VERSION=1.17.0
|
||||||
BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
|
BROTLI_VERSION=1.1.0 # Patched; next release won't need patching. See patch file.
|
||||||
|
@ -165,7 +165,7 @@ function build_brotli {
|
||||||
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
|
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
|
||||||
(cd $out_dir \
|
(cd $out_dir \
|
||||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
|
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
|
||||||
&& make install)
|
&& make -j4 install)
|
||||||
touch brotli-stamp
|
touch brotli-stamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,30 +186,43 @@ function build_libavif {
|
||||||
|
|
||||||
python3 -m pip install meson ninja
|
python3 -m pip install meson ninja
|
||||||
|
|
||||||
if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then
|
if ([[ "$PLAT" == "x86_64" ]] && [[ -z "$IOS_SDK" ]]) || [ -n "$SANITIZER" ]; then
|
||||||
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
|
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local build_type=MinSizeRel
|
local build_type=MinSizeRel
|
||||||
|
local build_shared=ON
|
||||||
local lto=ON
|
local lto=ON
|
||||||
|
|
||||||
local libavif_cmake_flags
|
local libavif_cmake_flags
|
||||||
|
|
||||||
if [ -n "$IS_MACOS" ]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
lto=OFF
|
lto=OFF
|
||||||
libavif_cmake_flags=(
|
libavif_cmake_flags=(
|
||||||
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
|
-DCMAKE_C_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
|
||||||
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
|
-DCMAKE_CXX_FLAGS_MINSIZEREL="-Oz -DNDEBUG -flto" \
|
||||||
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
|
-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,-S,-x,-dead_strip_dylibs" \
|
||||||
)
|
)
|
||||||
|
if [[ -n "$IOS_SDK" ]]; then
|
||||||
|
build_shared=OFF
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then
|
if [[ "$MB_ML_VER" == 2014 ]] && [[ "$PLAT" == "x86_64" ]]; then
|
||||||
build_type=Release
|
build_type=Release
|
||||||
fi
|
fi
|
||||||
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
|
libavif_cmake_flags=(-DCMAKE_SHARED_LINKER_FLAGS_INIT="-Wl,--strip-all,-z,relro,-z,now")
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "$IOS_SDK" ]] && [[ "$PLAT" == "x86_64" ]]; then
|
||||||
|
libavif_cmake_flags+=(-DAOM_TARGET_CPU=generic)
|
||||||
|
else
|
||||||
|
libavif_cmake_flags+=(
|
||||||
|
-DAVIF_CODEC_AOM_DECODE=OFF \
|
||||||
|
-DAVIF_CODEC_DAV1D=LOCAL
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
|
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
|
||||||
|
|
||||||
# CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
|
# CONFIG_AV1_HIGHBITDEPTH=0 is a flag for libaom (included as a subproject
|
||||||
# of libavif) that disables support for encoding high bit depth images.
|
# of libavif) that disables support for encoding high bit depth images.
|
||||||
(cd $out_dir \
|
(cd $out_dir \
|
||||||
|
@ -217,33 +230,44 @@ function build_libavif {
|
||||||
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
|
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
|
||||||
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
|
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
|
||||||
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
|
-DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib \
|
||||||
-DBUILD_SHARED_LIBS=ON \
|
-DBUILD_SHARED_LIBS=$build_shared \
|
||||||
-DAVIF_LIBSHARPYUV=LOCAL \
|
-DAVIF_LIBSHARPYUV=LOCAL \
|
||||||
-DAVIF_LIBYUV=LOCAL \
|
-DAVIF_LIBYUV=LOCAL \
|
||||||
-DAVIF_CODEC_AOM=LOCAL \
|
-DAVIF_CODEC_AOM=LOCAL \
|
||||||
-DCONFIG_AV1_HIGHBITDEPTH=0 \
|
-DCONFIG_AV1_HIGHBITDEPTH=0 \
|
||||||
-DAVIF_CODEC_AOM_DECODE=OFF \
|
|
||||||
-DAVIF_CODEC_DAV1D=LOCAL \
|
|
||||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
|
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=$lto \
|
||||||
-DCMAKE_C_VISIBILITY_PRESET=hidden \
|
-DCMAKE_C_VISIBILITY_PRESET=hidden \
|
||||||
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
|
-DCMAKE_CXX_VISIBILITY_PRESET=hidden \
|
||||||
-DCMAKE_BUILD_TYPE=$build_type \
|
-DCMAKE_BUILD_TYPE=$build_type \
|
||||||
"${libavif_cmake_flags[@]}" \
|
"${libavif_cmake_flags[@]}" \
|
||||||
. \
|
$HOST_CMAKE_FLAGS . )
|
||||||
&& make install)
|
|
||||||
|
if [[ -n "$IOS_SDK" ]]; then
|
||||||
|
# libavif's CMake configuration generates a meson cross file... but it
|
||||||
|
# doesn't work for iOS cross-compilation. Copy in Pillow-generated
|
||||||
|
# meson-cross config to replace the cmake-generated version.
|
||||||
|
cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd $out_dir && make -j4 install)
|
||||||
|
|
||||||
touch libavif-stamp
|
touch libavif-stamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function build_zstd {
|
||||||
|
if [ -e zstd-stamp ]; then return; fi
|
||||||
|
local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
|
||||||
|
(cd $out_dir \
|
||||||
|
&& make -j4 install)
|
||||||
|
touch zstd-stamp
|
||||||
|
}
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
build_xz
|
build_xz
|
||||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||||
yum remove -y zlib-devel
|
yum remove -y zlib-devel
|
||||||
fi
|
fi
|
||||||
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
|
|
||||||
build_new_zlib
|
|
||||||
else
|
|
||||||
build_zlib_ng
|
build_zlib_ng
|
||||||
fi
|
|
||||||
|
|
||||||
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
|
@ -265,13 +289,11 @@ function build {
|
||||||
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
|
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
|
||||||
--disable-webp --disable-libdeflate --disable-zstd
|
--disable-webp --disable-libdeflate --disable-zstd
|
||||||
else
|
else
|
||||||
|
build_zstd
|
||||||
build_tiff
|
build_tiff
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$IOS_SDK" ]]; then
|
|
||||||
# Short term workaround; don't build libavif on iOS
|
|
||||||
build_libavif
|
build_libavif
|
||||||
fi
|
|
||||||
build_libpng
|
build_libpng
|
||||||
build_lcms2
|
build_lcms2
|
||||||
build_openjpeg
|
build_openjpeg
|
||||||
|
@ -280,7 +302,11 @@ function build {
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
|
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
|
||||||
fi
|
fi
|
||||||
CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \
|
webp_ldflags=""
|
||||||
|
if [[ -n "$IOS_SDK" ]]; then
|
||||||
|
webp_ldflags="$webp_ldflags -llzma -lz"
|
||||||
|
fi
|
||||||
|
CFLAGS="$CFLAGS $webp_cflags" LDFLAGS="$LDFLAGS $webp_ldflags" build_simple libwebp $LIBWEBP_VERSION \
|
||||||
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
||||||
--enable-libwebpmux --enable-libwebpdemux
|
--enable-libwebpmux --enable-libwebpdemux
|
||||||
|
|
||||||
|
@ -380,6 +406,15 @@ fi
|
||||||
|
|
||||||
wrap_wheel_builder build
|
wrap_wheel_builder build
|
||||||
|
|
||||||
|
# A safety catch for iOS. iOS can't use dynamic libraries, but clang will prefer
|
||||||
|
# to link dynamic libraries to static libraries. The only way to reliably
|
||||||
|
# prevent this is to not have dynamic libraries available in the first place.
|
||||||
|
# The build process *shouldn't* generate any dylibs... but just in case, purge
|
||||||
|
# any dylibs that *have* been installed into the build prefix directory.
|
||||||
|
if [[ -n "$IOS_SDK" ]]; then
|
||||||
|
find "$BUILD_PREFIX" -name "*.dylib" -exec rm -rf {} \;
|
||||||
|
fi
|
||||||
|
|
||||||
# Return to the project root to finish the build
|
# Return to the project root to finish the build
|
||||||
popd > /dev/null
|
popd > /dev/null
|
||||||
|
|
||||||
|
|
18
.github/workflows/wheels.yml
vendored
18
.github/workflows/wheels.yml
vendored
|
@ -77,36 +77,36 @@ jobs:
|
||||||
platform: linux
|
platform: linux
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
cibw_arch: x86_64
|
cibw_arch: x86_64
|
||||||
|
manylinux: "manylinux2014"
|
||||||
- name: "manylinux_2_28 x86_64"
|
- name: "manylinux_2_28 x86_64"
|
||||||
platform: linux
|
platform: linux
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
cibw_arch: x86_64
|
cibw_arch: x86_64
|
||||||
build: "*manylinux*"
|
build: "*manylinux*"
|
||||||
manylinux: "manylinux_2_28"
|
|
||||||
- name: "manylinux2014 and musllinux aarch64"
|
- name: "manylinux2014 and musllinux aarch64"
|
||||||
platform: linux
|
platform: linux
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
cibw_arch: aarch64
|
cibw_arch: aarch64
|
||||||
|
manylinux: "manylinux2014"
|
||||||
- name: "manylinux_2_28 aarch64"
|
- name: "manylinux_2_28 aarch64"
|
||||||
platform: linux
|
platform: linux
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
cibw_arch: aarch64
|
cibw_arch: aarch64
|
||||||
build: "*manylinux*"
|
build: "*manylinux*"
|
||||||
manylinux: "manylinux_2_28"
|
|
||||||
- name: "iOS arm64 device"
|
- name: "iOS arm64 device"
|
||||||
platform: ios
|
platform: ios
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
cibw_arch: arm64_iphoneos
|
cibw_arch: arm64_iphoneos
|
||||||
- name: "iOS arm64 simulator"
|
- name: "iOS arm64 simulator"
|
||||||
platform: ios
|
platform: ios
|
||||||
os: macos-latest
|
os: macos-14
|
||||||
cibw_arch: arm64_iphonesimulator
|
cibw_arch: arm64_iphonesimulator
|
||||||
- name: "iOS x86_64 simulator"
|
- name: "iOS x86_64 simulator"
|
||||||
platform: ios
|
platform: ios
|
||||||
os: macos-13
|
os: macos-13
|
||||||
cibw_arch: x86_64_iphonesimulator
|
cibw_arch: x86_64_iphonesimulator
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: true
|
submodules: true
|
||||||
|
@ -153,12 +153,12 @@ jobs:
|
||||||
- cibw_arch: ARM64
|
- cibw_arch: ARM64
|
||||||
os: windows-11-arm
|
os: windows-11-arm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Checkout extra test images
|
- name: Checkout extra test images
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: python-pillow/test-images
|
repository: python-pillow/test-images
|
||||||
|
@ -234,7 +234,7 @@ jobs:
|
||||||
if: github.event_name != 'schedule'
|
if: github.event_name != 'schedule'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Upload wheels to scientific-python-nightly-wheels
|
name: Upload wheels to scientific-python-nightly-wheels
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: dist-*
|
pattern: dist-*
|
||||||
path: dist
|
path: dist
|
||||||
|
@ -278,7 +278,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: dist-*
|
pattern: dist-*
|
||||||
path: dist
|
path: dist
|
||||||
|
|
2
.github/zizmor.yml
vendored
2
.github/zizmor.yml
vendored
|
@ -1,5 +1,5 @@
|
||||||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||||
# https://woodruffw.github.io/zizmor/configuration/
|
# https://docs.zizmor.sh/configuration/
|
||||||
rules:
|
rules:
|
||||||
unpinned-uses:
|
unpinned-uses:
|
||||||
config:
|
config:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.2
|
rev: v0.12.11
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args: [--exit-non-zero-on-fix]
|
args: [--exit-non-zero-on-fix]
|
||||||
|
@ -24,7 +24,7 @@ repos:
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v20.1.7
|
rev: v21.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
types: [c]
|
types: [c]
|
||||||
|
@ -36,7 +36,7 @@ repos:
|
||||||
- id: rst-backticks
|
- id: rst-backticks
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
- id: check-shebang-scripts-are-executable
|
- id: check-shebang-scripts-are-executable
|
||||||
|
@ -51,14 +51,14 @@ repos:
|
||||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
|
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
|
||||||
|
|
||||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||||
rev: 0.33.2
|
rev: 0.33.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-github-workflows
|
- id: check-github-workflows
|
||||||
- id: check-readthedocs
|
- id: check-readthedocs
|
||||||
- id: check-renovate
|
- id: check-renovate
|
||||||
|
|
||||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||||
rev: v1.11.0
|
rev: v1.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ repos:
|
||||||
additional_dependencies: [trove-classifiers>=2024.10.12]
|
additional_dependencies: [trove-classifiers>=2024.10.12]
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||||
rev: 1.5.0
|
rev: 1.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: tox-ini-fmt
|
- id: tox-ini-fmt
|
||||||
|
|
||||||
|
|
12
MANIFEST.in
12
MANIFEST.in
|
@ -13,6 +13,7 @@ include LICENSE
|
||||||
include Makefile
|
include Makefile
|
||||||
include tox.ini
|
include tox.ini
|
||||||
graft Tests
|
graft Tests
|
||||||
|
graft Tests/images
|
||||||
graft checks
|
graft checks
|
||||||
graft patches
|
graft patches
|
||||||
graft src
|
graft src
|
||||||
|
@ -28,8 +29,19 @@ exclude .editorconfig
|
||||||
exclude .readthedocs.yml
|
exclude .readthedocs.yml
|
||||||
exclude codecov.yml
|
exclude codecov.yml
|
||||||
exclude renovate.json
|
exclude renovate.json
|
||||||
|
exclude Tests/images/README.md
|
||||||
|
exclude Tests/images/crash*.tif
|
||||||
|
exclude Tests/images/string_dimension.tiff
|
||||||
global-exclude .git*
|
global-exclude .git*
|
||||||
global-exclude *.pyc
|
global-exclude *.pyc
|
||||||
global-exclude *.so
|
global-exclude *.so
|
||||||
prune .ci
|
prune .ci
|
||||||
prune wheels
|
prune wheels
|
||||||
|
prune winbuild/build
|
||||||
|
prune winbuild/depends
|
||||||
|
prune Tests/errors
|
||||||
|
prune Tests/images/jpeg2000
|
||||||
|
prune Tests/images/msp
|
||||||
|
prune Tests/images/picins
|
||||||
|
prune Tests/images/sunraster
|
||||||
|
prune Tests/test-images
|
||||||
|
|
|
@ -36,9 +36,6 @@ As of 2019, Pillow development is
|
||||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
|
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
|
||||||
alt="GitHub Actions build status (Test MinGW)"
|
alt="GitHub Actions build status (Test MinGW)"
|
||||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
|
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
|
||||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
|
|
||||||
alt="GitHub Actions build status (Test Cygwin)"
|
|
||||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
|
|
||||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
|
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
|
||||||
alt="GitHub Actions build status (Test Docker)"
|
alt="GitHub Actions build status (Test Docker)"
|
||||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
|
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
|
||||||
|
|
|
@ -10,17 +10,20 @@ import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections.abc import Sequence
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image, ImageFile, ImageMath, features
|
from PIL import Image, ImageFile, ImageMath, features
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
uploader = None
|
uploader = None
|
||||||
|
@ -172,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
|
||||||
return pytest.mark.skipif(not features.check(feature), reason=reason)
|
return pytest.mark.skipif(not features.check(feature), reason=reason)
|
||||||
|
|
||||||
|
|
||||||
|
def has_feature_version(feature: str, required: str) -> bool:
|
||||||
|
version = features.version(feature)
|
||||||
|
assert version is not None
|
||||||
|
version_required = parse_version(required)
|
||||||
|
version_available = parse_version(version)
|
||||||
|
return version_available >= version_required
|
||||||
|
|
||||||
|
|
||||||
def skip_unless_feature_version(
|
def skip_unless_feature_version(
|
||||||
feature: str, required: str, reason: str | None = None
|
feature: str, required: str, reason: str | None = None
|
||||||
) -> pytest.MarkDecorator:
|
) -> pytest.MarkDecorator:
|
||||||
|
@ -291,16 +302,6 @@ def djpeg_available() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def cjpeg_available() -> bool:
|
|
||||||
if shutil.which("cjpeg"):
|
|
||||||
try:
|
|
||||||
subprocess.check_call(["cjpeg", "-version"])
|
|
||||||
return True
|
|
||||||
except subprocess.CalledProcessError: # pragma: no cover
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def netpbm_available() -> bool:
|
def netpbm_available() -> bool:
|
||||||
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
|
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
|
||||||
|
|
||||||
|
|
BIN
Tests/images/unimplemented_pixel_format.dds
Normal file
BIN
Tests/images/unimplemented_pixel_format.dds
Normal file
Binary file not shown.
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -10,6 +9,10 @@ from PIL import features
|
||||||
|
|
||||||
from .helper import skip_unless_feature
|
from .helper import skip_unless_feature
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
def test_check() -> None:
|
def test_check() -> None:
|
||||||
# Check the correctness of the convenience function
|
# Check the correctness of the convenience function
|
||||||
|
@ -18,10 +21,6 @@ def test_check() -> None:
|
||||||
for codec in features.codecs:
|
for codec in features.codecs:
|
||||||
assert features.check_codec(codec) == features.check(codec)
|
assert features.check_codec(codec) == features.check(codec)
|
||||||
for feature in features.features:
|
for feature in features.features:
|
||||||
if "webp" in feature:
|
|
||||||
with pytest.warns(DeprecationWarning, match="webp"):
|
|
||||||
assert features.check_feature(feature) == features.check(feature)
|
|
||||||
else:
|
|
||||||
assert features.check_feature(feature) == features.check(feature)
|
assert features.check_feature(feature) == features.check(feature)
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,10 +47,6 @@ def test_version() -> None:
|
||||||
for codec in features.codecs:
|
for codec in features.codecs:
|
||||||
test(codec, features.version_codec)
|
test(codec, features.version_codec)
|
||||||
for feature in features.features:
|
for feature in features.features:
|
||||||
if "webp" in feature:
|
|
||||||
with pytest.warns(DeprecationWarning, match="webp"):
|
|
||||||
test(feature, features.version_feature)
|
|
||||||
else:
|
|
||||||
test(feature, features.version_feature)
|
test(feature, features.version_feature)
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,6 +107,25 @@ def test_unsupported_module() -> None:
|
||||||
features.version_module(module)
|
features.version_module(module)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_feature() -> None:
|
||||||
|
# Arrange
|
||||||
|
feature = "unsupported_feature"
|
||||||
|
# Act / Assert
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
features.check_feature(feature)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
features.version_feature(feature)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_version() -> None:
|
||||||
|
assert features.version("unsupported_version") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_modulenotfound(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(features, "features", {"test": ("PIL._test", "", "")})
|
||||||
|
assert features.check_feature("test") is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("supported_formats", (True, False))
|
@pytest.mark.parametrize("supported_formats", (True, False))
|
||||||
def test_pilinfo(supported_formats: bool) -> None:
|
def test_pilinfo(supported_formats: bool) -> None:
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
|
|
|
@ -380,21 +380,28 @@ def test_palette() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_header_size() -> None:
|
||||||
|
with pytest.raises(OSError, match="Unsupported header size 0"):
|
||||||
|
with Image.open(BytesIO(b"DDS " + b"\x00" * 4)):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_unsupported_bitcount() -> None:
|
def test_unsupported_bitcount() -> None:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError, match="Unsupported bitcount 24 for 131072"):
|
||||||
with Image.open("Tests/images/unsupported_bitcount.dds"):
|
with Image.open("Tests/images/unsupported_bitcount.dds"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file",
|
"test_file, message",
|
||||||
(
|
(
|
||||||
"Tests/images/unimplemented_dxgi_format.dds",
|
("Tests/images/unimplemented_dxgi_format.dds", "Unimplemented DXGI format 93"),
|
||||||
"Tests/images/unimplemented_pfflags.dds",
|
("Tests/images/unimplemented_pixel_format.dds", "Unimplemented pixel format 0"),
|
||||||
|
("Tests/images/unimplemented_pfflags.dds", "Unknown pixel format flags 8"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_not_implemented(test_file: str) -> None:
|
def test_not_implemented(test_file: str, message: str) -> None:
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError, match=message):
|
||||||
with Image.open(test_file):
|
with Image.open(test_file):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import GbrImagePlugin, Image
|
from PIL import GbrImagePlugin, Image, _binary
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile
|
from .helper import assert_image_equal_tofile
|
||||||
|
|
||||||
|
@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file() -> None:
|
def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
return BytesIO(
|
||||||
|
b"".join(
|
||||||
|
_binary.o32be(i)
|
||||||
|
for i in [
|
||||||
|
info.get("header_size", 20),
|
||||||
|
info.get("version", 1),
|
||||||
|
info.get("width", 1),
|
||||||
|
info.get("height", 1),
|
||||||
|
info.get("color_depth", 1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ magic_number
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
|
||||||
|
def test_invalid_file() -> None:
|
||||||
|
for f in [
|
||||||
|
create_gbr_image({"header_size": 0}),
|
||||||
|
create_gbr_image({"width": 0}),
|
||||||
|
create_gbr_image({"height": 0}),
|
||||||
|
]:
|
||||||
|
with pytest.raises(SyntaxError, match="not a GIMP brush"):
|
||||||
|
GbrImagePlugin.GbrImageFile(f)
|
||||||
|
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"):
|
||||||
GbrImagePlugin.GbrImageFile(invalid_file)
|
GbrImagePlugin.GbrImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_gimp_brush() -> None:
|
||||||
|
f = create_gbr_image({"color_depth": 2})
|
||||||
|
with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"):
|
||||||
|
GbrImagePlugin.GbrImageFile(f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_magic_number() -> None:
|
||||||
|
f = create_gbr_image({"version": 2}, magic_number=b"badm")
|
||||||
|
with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"):
|
||||||
|
GbrImagePlugin.GbrImageFile(f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_L() -> None:
|
||||||
|
f = create_gbr_image()
|
||||||
|
with Image.open(f) as im:
|
||||||
|
assert im.mode == "L"
|
||||||
|
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
|
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
@ -9,21 +11,78 @@ from .helper import assert_image_equal, hopper
|
||||||
TEST_FILE = "Tests/images/iptc.jpg"
|
TEST_FILE = "Tests/images/iptc.jpg"
|
||||||
|
|
||||||
|
|
||||||
|
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
|
||||||
|
def field(tag, value):
|
||||||
|
return bytes((0x1C,) + tag + (0, len(value))) + value
|
||||||
|
|
||||||
|
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
|
||||||
|
data += field((3, 120), bytes((info.get("compression", 1),)))
|
||||||
|
if "band" in info:
|
||||||
|
data += field((3, 65), bytes((info["band"] + 1,)))
|
||||||
|
data += field((3, 20), b"\x01") # width
|
||||||
|
data += field((3, 30), b"\x01") # height
|
||||||
|
data += field(
|
||||||
|
(8, 10),
|
||||||
|
bytes((info.get("data", 0),)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return BytesIO(data)
|
||||||
|
|
||||||
|
|
||||||
def test_open() -> None:
|
def test_open() -> None:
|
||||||
expected = Image.new("L", (1, 1))
|
expected = Image.new("L", (1, 1))
|
||||||
|
|
||||||
f = BytesIO(
|
f = create_iptc_image()
|
||||||
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
|
|
||||||
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
|
|
||||||
)
|
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
|
assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))]
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
assert im.load() is not None
|
assert im.load() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_field_length() -> None:
|
||||||
|
f = create_iptc_image()
|
||||||
|
f.seek(28)
|
||||||
|
f.write(b"\xff")
|
||||||
|
with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"):
|
||||||
|
with Image.open(f):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK")))
|
||||||
|
def test_layers(layers: int, mode: str) -> None:
|
||||||
|
for band in range(-1, layers):
|
||||||
|
info = {"layers": layers, "component": 1, "data": 5}
|
||||||
|
if band != -1:
|
||||||
|
info["band"] = band
|
||||||
|
f = create_iptc_image(info)
|
||||||
|
with Image.open(f) as im:
|
||||||
|
assert im.mode == mode
|
||||||
|
|
||||||
|
data = [0] * layers
|
||||||
|
data[max(band, 0)] = 5
|
||||||
|
assert im.getpixel((0, 0)) == tuple(data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_compression() -> None:
|
||||||
|
f = create_iptc_image({"compression": 2})
|
||||||
|
with pytest.raises(OSError, match="Unknown IPTC image compression"):
|
||||||
|
with Image.open(f):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_getiptcinfo() -> None:
|
||||||
|
f = create_iptc_image()
|
||||||
|
with Image.open(f) as im:
|
||||||
|
assert IptcImagePlugin.getiptcinfo(im) == {
|
||||||
|
(3, 60): b"\x01\x00",
|
||||||
|
(3, 120): b"\x01",
|
||||||
|
(3, 20): b"\x01",
|
||||||
|
(3, 30): b"\x01",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_getiptcinfo_jpg_none() -> None:
|
def test_getiptcinfo_jpg_none() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
|
|
|
@ -26,7 +26,6 @@ from .helper import (
|
||||||
assert_image_equal_tofile,
|
assert_image_equal_tofile,
|
||||||
assert_image_similar,
|
assert_image_similar,
|
||||||
assert_image_similar_tofile,
|
assert_image_similar_tofile,
|
||||||
cjpeg_available,
|
|
||||||
djpeg_available,
|
djpeg_available,
|
||||||
hopper,
|
hopper,
|
||||||
is_win32,
|
is_win32,
|
||||||
|
@ -731,14 +730,6 @@ class TestFileJpeg:
|
||||||
img.load_djpeg()
|
img.load_djpeg()
|
||||||
assert_image_similar_tofile(img, TEST_FILE, 5)
|
assert_image_similar_tofile(img, TEST_FILE, 5)
|
||||||
|
|
||||||
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
|
|
||||||
def test_save_cjpeg(self, tmp_path: Path) -> None:
|
|
||||||
with Image.open(TEST_FILE) as img:
|
|
||||||
tempfile = str(tmp_path / "temp.jpg")
|
|
||||||
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
|
|
||||||
# Default save quality is 75%, so a tiny bit of difference is alright
|
|
||||||
assert_image_similar_tofile(img, tempfile, 17)
|
|
||||||
|
|
||||||
def test_no_duplicate_0x1001_tag(self) -> None:
|
def test_no_duplicate_0x1001_tag(self) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
tag_ids = {v: k for k, v in ExifTags.TAGS.items()}
|
tag_ids = {v: k for k, v in ExifTags.TAGS.items()}
|
||||||
|
|
|
@ -365,7 +365,6 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
if 700 in reloaded.tag_v2:
|
|
||||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||||
|
|
||||||
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||||
|
@ -873,7 +872,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
assert im.format == "TIFF"
|
assert im.format == "TIFF"
|
||||||
im2 = hopper()
|
with hopper() as im2:
|
||||||
assert_image_similar(im, im2, 5)
|
assert_image_similar(im, im2, 5)
|
||||||
except OSError:
|
except OSError:
|
||||||
captured = capfd.readouterr()
|
captured = capfd.readouterr()
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
def test_load_raw() -> None:
|
def test_load_raw() -> None:
|
||||||
with Image.open("Tests/images/hopper.pcd") as im:
|
with Image.open("Tests/images/hopper.pcd") as im:
|
||||||
|
assert im.size == (768, 512)
|
||||||
im.load() # should not segfault.
|
im.load() # should not segfault.
|
||||||
|
|
||||||
# Note that this image was created with a resized hopper
|
# Note that this image was created with a resized hopper
|
||||||
|
@ -15,3 +20,13 @@ def test_load_raw() -> None:
|
||||||
|
|
||||||
# target = hopper().resize((768,512))
|
# target = hopper().resize((768,512))
|
||||||
# assert_image_similar(im, target, 10)
|
# assert_image_similar(im, target, 10)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("orientation", (1, 3))
|
||||||
|
def test_rotated(orientation: int) -> None:
|
||||||
|
with open("Tests/images/hopper.pcd", "rb") as fp:
|
||||||
|
data = bytearray(fp.read())
|
||||||
|
data[2048 + 1538] = orientation
|
||||||
|
f = BytesIO(data)
|
||||||
|
with Image.open(f) as im:
|
||||||
|
assert im.size == (512, 768)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from PIL import WalImageFile
|
from PIL import WalImageFile
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile
|
from .helper import assert_image_equal_tofile
|
||||||
|
@ -13,12 +15,22 @@ def test_open() -> None:
|
||||||
assert im.format_description == "Quake2 Texture"
|
assert im.format_description == "Quake2 Texture"
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
assert "next_name" not in im.info
|
||||||
|
|
||||||
assert isinstance(im, WalImageFile.WalImageFile)
|
assert isinstance(im, WalImageFile.WalImageFile)
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
|
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_name() -> None:
|
||||||
|
with open(TEST_FILE, "rb") as fp:
|
||||||
|
data = bytearray(fp.read())
|
||||||
|
data[56:60] = b"Test"
|
||||||
|
f = BytesIO(data)
|
||||||
|
with WalImageFile.open(f) as im:
|
||||||
|
assert im.info["next_name"] == b"Test"
|
||||||
|
|
||||||
|
|
||||||
def test_load() -> None:
|
def test_load() -> None:
|
||||||
with WalImageFile.open(TEST_FILE) as im:
|
with WalImageFile.open(TEST_FILE) as im:
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
|
|
@ -4,13 +4,13 @@ from collections.abc import Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
|
||||||
|
|
||||||
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
|
from PIL import GifImagePlugin, Image, WebPImagePlugin
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
assert_image_similar,
|
assert_image_similar,
|
||||||
|
has_feature_version,
|
||||||
is_big_endian,
|
is_big_endian,
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
@ -53,10 +53,7 @@ def test_write_animation_L(tmp_path: Path) -> None:
|
||||||
im.load()
|
im.load()
|
||||||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||||
|
|
||||||
if is_big_endian():
|
if is_big_endian() and not has_feature_version("webp", "1.2.2"):
|
||||||
version = features.version_module("webp")
|
|
||||||
assert version is not None
|
|
||||||
if parse_version(version) < parse_version("1.2.2"):
|
|
||||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||||
orig.seek(orig.n_frames - 1)
|
orig.seek(orig.n_frames - 1)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
@ -81,10 +78,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
||||||
assert_image_equal(im, frame1.convert("RGBA"))
|
assert_image_equal(im, frame1.convert("RGBA"))
|
||||||
|
|
||||||
# Compare second frame to original
|
# Compare second frame to original
|
||||||
if is_big_endian():
|
if is_big_endian() and not has_feature_version("webp", "1.2.2"):
|
||||||
version = features.version_module("webp")
|
|
||||||
assert version is not None
|
|
||||||
if parse_version(version) < parse_version("1.2.2"):
|
|
||||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
|
@ -44,6 +44,18 @@ def test_load_zero_inch() -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_unsupported_wmf() -> None:
|
||||||
|
b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x01" * 10)
|
||||||
|
with pytest.raises(SyntaxError, match="Unsupported WMF file format"):
|
||||||
|
WmfImagePlugin.WmfStubImageFile(b)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_unsupported() -> None:
|
||||||
|
b = BytesIO(b"\x01\x00\x00\x00")
|
||||||
|
with pytest.raises(SyntaxError, match="Unsupported file format"):
|
||||||
|
WmfImagePlugin.WmfStubImageFile(b)
|
||||||
|
|
||||||
|
|
||||||
def test_render() -> None:
|
def test_render() -> None:
|
||||||
with open("Tests/images/drawing.emf", "rb") as fp:
|
with open("Tests/images/drawing.emf", "rb") as fp:
|
||||||
data = fp.read()
|
data = fp.read()
|
||||||
|
|
|
@ -9,7 +9,8 @@ from .helper import skip_unless_feature
|
||||||
|
|
||||||
class TestFontCrash:
|
class TestFontCrash:
|
||||||
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
|
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
|
||||||
# from fuzzers.fuzz_font
|
# Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py
|
||||||
|
# that triggered a problem when fuzzing
|
||||||
font.getbbox("ABC")
|
font.getbbox("ABC")
|
||||||
font.getmask("test text")
|
font.getmask("test text")
|
||||||
with Image.new(mode="RGBA", size=(200, 200)) as im:
|
with Image.new(mode="RGBA", size=(200, 200)) as im:
|
||||||
|
|
|
@ -2,12 +2,15 @@ from __future__ import annotations
|
||||||
|
|
||||||
import colorsys
|
import colorsys
|
||||||
import itertools
|
import itertools
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import assert_image_similar, hopper
|
from .helper import assert_image_similar, hopper
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
def int_to_float(i: int) -> float:
|
def int_to_float(i: int) -> float:
|
||||||
return i / 255
|
return i / 255
|
||||||
|
|
|
@ -19,6 +19,7 @@ from PIL import (
|
||||||
ImageDraw,
|
ImageDraw,
|
||||||
ImageFile,
|
ImageFile,
|
||||||
ImagePalette,
|
ImagePalette,
|
||||||
|
ImageShow,
|
||||||
TiffImagePlugin,
|
TiffImagePlugin,
|
||||||
UnidentifiedImageError,
|
UnidentifiedImageError,
|
||||||
features,
|
features,
|
||||||
|
@ -389,6 +390,37 @@ class TestImage:
|
||||||
assert img_colors is not None
|
assert img_colors is not None
|
||||||
assert sorted(img_colors) == expected_colors
|
assert sorted(img_colors) == expected_colors
|
||||||
|
|
||||||
|
def test_alpha_composite_la(self) -> None:
|
||||||
|
# Arrange
|
||||||
|
expected_colors = sorted(
|
||||||
|
[
|
||||||
|
(3300, (255, 255)),
|
||||||
|
(1156, (170, 192)),
|
||||||
|
(1122, (128, 255)),
|
||||||
|
(1089, (0, 0)),
|
||||||
|
(1122, (255, 128)),
|
||||||
|
(1122, (0, 128)),
|
||||||
|
(1089, (0, 255)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
dst = Image.new("LA", size=(100, 100), color=(0, 255))
|
||||||
|
draw = ImageDraw.Draw(dst)
|
||||||
|
draw.rectangle((0, 33, 100, 66), fill=(0, 128))
|
||||||
|
draw.rectangle((0, 67, 100, 100), fill=(0, 0))
|
||||||
|
src = Image.new("LA", size=(100, 100), color=(255, 255))
|
||||||
|
draw = ImageDraw.Draw(src)
|
||||||
|
draw.rectangle((33, 0, 66, 100), fill=(255, 128))
|
||||||
|
draw.rectangle((67, 0, 100, 100), fill=(255, 0))
|
||||||
|
|
||||||
|
# Act
|
||||||
|
img = Image.alpha_composite(dst, src)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
img_colors = img.getcolors()
|
||||||
|
assert img_colors is not None
|
||||||
|
assert sorted(img_colors) == expected_colors
|
||||||
|
|
||||||
def test_alpha_inplace(self) -> None:
|
def test_alpha_inplace(self) -> None:
|
||||||
src = Image.new("RGBA", (128, 128), "blue")
|
src = Image.new("RGBA", (128, 128), "blue")
|
||||||
|
|
||||||
|
@ -994,6 +1026,17 @@ class TestImage:
|
||||||
reloaded_exif.load(exif.tobytes())
|
reloaded_exif.load(exif.tobytes())
|
||||||
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
|
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
|
||||||
|
|
||||||
|
def test_delete_ifd_tag(self) -> None:
|
||||||
|
with Image.open("Tests/images/flower.jpg") as im:
|
||||||
|
exif = im.getexif()
|
||||||
|
exif.get_ifd(0x8769)
|
||||||
|
assert 0x8769 in exif
|
||||||
|
del exif[0x8769]
|
||||||
|
|
||||||
|
reloaded_exif = Image.Exif()
|
||||||
|
reloaded_exif.load(exif.tobytes())
|
||||||
|
assert 0x8769 not in reloaded_exif
|
||||||
|
|
||||||
def test_exif_load_from_fp(self) -> None:
|
def test_exif_load_from_fp(self) -> None:
|
||||||
with Image.open("Tests/images/flower.jpg") as im:
|
with Image.open("Tests/images/flower.jpg") as im:
|
||||||
data = im.info["exif"]
|
data = im.info["exif"]
|
||||||
|
@ -1077,6 +1120,13 @@ class TestImage:
|
||||||
with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
|
with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
|
||||||
assert im.get_child_images() == []
|
assert im.get_child_images() == []
|
||||||
|
|
||||||
|
def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(ImageShow, "_viewers", [])
|
||||||
|
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
with pytest.warns(DeprecationWarning, match="Image._show"):
|
||||||
|
Image._show(im)
|
||||||
|
|
||||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
|
|
|
@ -315,3 +315,10 @@ int main(int argc, char* argv[])
|
||||||
process = subprocess.Popen(["embed_pil.exe"], env=env)
|
process = subprocess.Popen(["embed_pil.exe"], env=env)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
assert process.returncode == 0
|
assert process.returncode == 0
|
||||||
|
|
||||||
|
def teardown_method(self) -> None:
|
||||||
|
try:
|
||||||
|
os.remove("embed_pil.c")
|
||||||
|
except FileNotFoundError:
|
||||||
|
# If the test was skipped or failed, the file won't exist
|
||||||
|
pass
|
||||||
|
|
|
@ -10,9 +10,12 @@ def test_histogram() -> None:
|
||||||
|
|
||||||
assert histogram("1") == (256, 0, 10994)
|
assert histogram("1") == (256, 0, 10994)
|
||||||
assert histogram("L") == (256, 0, 662)
|
assert histogram("L") == (256, 0, 662)
|
||||||
|
assert histogram("LA") == (512, 0, 16384)
|
||||||
|
assert histogram("La") == (512, 0, 16384)
|
||||||
assert histogram("I") == (256, 0, 662)
|
assert histogram("I") == (256, 0, 662)
|
||||||
assert histogram("F") == (256, 0, 662)
|
assert histogram("F") == (256, 0, 662)
|
||||||
assert histogram("P") == (256, 0, 1551)
|
assert histogram("P") == (256, 0, 1551)
|
||||||
|
assert histogram("PA") == (512, 0, 16384)
|
||||||
assert histogram("RGB") == (768, 4, 675)
|
assert histogram("RGB") == (768, 4, 675)
|
||||||
assert histogram("RGBA") == (1024, 0, 16384)
|
assert histogram("RGBA") == (1024, 0, 16384)
|
||||||
assert histogram("CMYK") == (1024, 0, 16384)
|
assert histogram("CMYK") == (1024, 0, 16384)
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
|
||||||
|
|
||||||
from PIL import Image, features
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature
|
from .helper import (
|
||||||
|
assert_image_similar,
|
||||||
|
has_feature_version,
|
||||||
|
hopper,
|
||||||
|
is_ppc64le,
|
||||||
|
skip_unless_feature,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_sanity() -> None:
|
def test_sanity() -> None:
|
||||||
|
@ -23,10 +28,7 @@ def test_sanity() -> None:
|
||||||
@skip_unless_feature("libimagequant")
|
@skip_unless_feature("libimagequant")
|
||||||
def test_libimagequant_quantize() -> None:
|
def test_libimagequant_quantize() -> None:
|
||||||
image = hopper()
|
image = hopper()
|
||||||
if is_ppc64le():
|
if is_ppc64le() and not has_feature_version("libimagequant", "4"):
|
||||||
version = features.version_feature("libimagequant")
|
|
||||||
assert version is not None
|
|
||||||
if parse_version(version) < parse_version("4"):
|
|
||||||
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
|
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
|
||||||
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
|
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
|
||||||
assert converted.mode == "P"
|
assert converted.mode == "P"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -9,6 +8,10 @@ from PIL import Image, ImageTransform
|
||||||
|
|
||||||
from .helper import assert_image_equal, assert_image_similar, hopper
|
from .helper import assert_image_equal, assert_image_similar, hopper
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
class TestImageTransform:
|
class TestImageTransform:
|
||||||
def test_sanity(self) -> None:
|
def test_sanity(self) -> None:
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from PIL import Image, ImageChops
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
BLACK = (0, 0, 0)
|
BLACK = (0, 0, 0)
|
||||||
BROWN = (127, 64, 0)
|
BROWN = (127, 64, 0)
|
||||||
CYAN = (0, 255, 255)
|
CYAN = (0, 255, 255)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, cast
|
from typing import Literal, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -31,6 +31,9 @@ except ImportError:
|
||||||
# Skipped via setup_module()
|
# Skipped via setup_module()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc"
|
SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc"
|
||||||
HAVE_PROFILE = os.path.exists(SRGB)
|
HAVE_PROFILE = os.path.exists(SRGB)
|
||||||
|
@ -208,9 +211,10 @@ def test_exceptions() -> None:
|
||||||
ImageCms.getProfileName(None) # type: ignore[arg-type]
|
ImageCms.getProfileName(None) # type: ignore[arg-type]
|
||||||
skip_missing()
|
skip_missing()
|
||||||
|
|
||||||
# Python <= 3.9: "an integer is required (got type NoneType)"
|
with pytest.raises(
|
||||||
# Python > 3.9: "'NoneType' object cannot be interpreted as an integer"
|
ImageCms.PyCMSError,
|
||||||
with pytest.raises(ImageCms.PyCMSError, match="integer"):
|
match="'NoneType' object cannot be interpreted as an integer",
|
||||||
|
):
|
||||||
ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type]
|
ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
@ -690,3 +694,17 @@ def test_cmyk_lab() -> None:
|
||||||
im = Image.new("CMYK", (1, 1))
|
im = Image.new("CMYK", (1, 1))
|
||||||
converted_im = im.convert("LAB")
|
converted_im = im.convert("LAB")
|
||||||
assert converted_im.getpixel((0, 0)) == (255, 128, 128)
|
assert converted_im.getpixel((0, 0)) == (255, 128, 128)
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecation() -> None:
|
||||||
|
profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
|
||||||
|
with pytest.warns(
|
||||||
|
DeprecationWarning, match="ImageCms.ImageCmsProfile.product_name"
|
||||||
|
):
|
||||||
|
profile.product_name
|
||||||
|
with pytest.warns(
|
||||||
|
DeprecationWarning, match="ImageCms.ImageCmsProfile.product_info"
|
||||||
|
):
|
||||||
|
profile.product_info
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
profile.this_attribute_does_not_exist
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
from collections.abc import Sequence
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
|
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
|
||||||
from PIL._typing import Coords
|
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -17,6 +14,12 @@ from .helper import (
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
|
||||||
|
from PIL._typing import Coords
|
||||||
|
|
||||||
BLACK = (0, 0, 0)
|
BLACK = (0, 0, 0)
|
||||||
WHITE = (255, 255, 255)
|
WHITE = (255, 255, 255)
|
||||||
GRAY = (190, 190, 190)
|
GRAY = (190, 190, 190)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from PIL import Image, ImageDraw, ImageFont
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal_tofile,
|
assert_image_equal_tofile,
|
||||||
assert_image_similar_tofile,
|
assert_image_similar_tofile,
|
||||||
|
has_feature_version,
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None:
|
||||||
|
|
||||||
im = Image.new(mode="RGB", size=(100, 300))
|
im = Image.new(mode="RGB", size=(100, 300))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
try:
|
if not has_feature_version("raqm", "0.7"):
|
||||||
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
|
|
||||||
except ValueError as ex:
|
|
||||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
|
||||||
pytest.skip("libraqm 0.7 or greater not available")
|
pytest.skip("libraqm 0.7 or greater not available")
|
||||||
|
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
|
||||||
|
|
||||||
target = "Tests/images/test_direction_ttb.png"
|
target = "Tests/images/test_direction_ttb.png"
|
||||||
assert_image_similar_tofile(im, target, 2.8)
|
assert_image_similar_tofile(im, target, 2.8)
|
||||||
|
@ -119,7 +118,8 @@ def test_text_direction_ttb_stroke() -> None:
|
||||||
|
|
||||||
im = Image.new(mode="RGB", size=(100, 300))
|
im = Image.new(mode="RGB", size=(100, 300))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
try:
|
if not has_feature_version("raqm", "0.7"):
|
||||||
|
pytest.skip("libraqm 0.7 or greater not available")
|
||||||
draw.text(
|
draw.text(
|
||||||
(27, 27),
|
(27, 27),
|
||||||
"あい",
|
"あい",
|
||||||
|
@ -129,9 +129,6 @@ def test_text_direction_ttb_stroke() -> None:
|
||||||
stroke_width=2,
|
stroke_width=2,
|
||||||
stroke_fill="#0f0",
|
stroke_fill="#0f0",
|
||||||
)
|
)
|
||||||
except ValueError as ex:
|
|
||||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
|
||||||
pytest.skip("libraqm 0.7 or greater not available")
|
|
||||||
|
|
||||||
target = "Tests/images/test_direction_ttb_stroke.png"
|
target = "Tests/images/test_direction_ttb_stroke.png"
|
||||||
assert_image_similar_tofile(im, target, 19.4)
|
assert_image_similar_tofile(im, target, 19.4)
|
||||||
|
@ -219,14 +216,9 @@ def test_getlength(
|
||||||
im = Image.new(mode, (1, 1), 0)
|
im = Image.new(mode, (1, 1), 0)
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
try:
|
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
|
||||||
assert d.textlength(text, ttf, direction) == expected
|
|
||||||
except ValueError as ex:
|
|
||||||
if (
|
|
||||||
direction == "ttb"
|
|
||||||
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
|
|
||||||
):
|
|
||||||
pytest.skip("libraqm 0.7 or greater not available")
|
pytest.skip("libraqm 0.7 or greater not available")
|
||||||
|
assert d.textlength(text, ttf, direction) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "1"))
|
@pytest.mark.parametrize("mode", ("L", "1"))
|
||||||
|
@ -242,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None:
|
||||||
|
|
||||||
ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
||||||
|
|
||||||
try:
|
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
|
||||||
|
pytest.skip("libraqm 0.7 or greater not available")
|
||||||
target = ttf.getlength("ii", mode, direction)
|
target = ttf.getlength("ii", mode, direction)
|
||||||
actual = ttf.getlength(text, mode, direction)
|
actual = ttf.getlength(text, mode, direction)
|
||||||
|
|
||||||
assert actual == target
|
assert actual == target
|
||||||
except ValueError as ex:
|
|
||||||
if (
|
|
||||||
direction == "ttb"
|
|
||||||
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
|
|
||||||
):
|
|
||||||
pytest.skip("libraqm 0.7 or greater not available")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
|
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
|
||||||
|
@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None:
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.line(((0, 200), (200, 200)), "gray")
|
d.line(((0, 200), (200, 200)), "gray")
|
||||||
d.line(((100, 0), (100, 400)), "gray")
|
d.line(((100, 0), (100, 400)), "gray")
|
||||||
try:
|
if not has_feature_version("raqm", "0.7"):
|
||||||
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
|
|
||||||
except ValueError as ex:
|
|
||||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
|
||||||
pytest.skip("libraqm 0.7 or greater not available")
|
pytest.skip("libraqm 0.7 or greater not available")
|
||||||
|
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
|
||||||
|
|
||||||
assert_image_similar_tofile(im, path, 1) # fails at 5
|
assert_image_similar_tofile(im, path, 1) # fails at 5
|
||||||
|
|
||||||
|
@ -310,10 +295,12 @@ combine_tests = (
|
||||||
|
|
||||||
# this tests various combining characters for anchor alignment and clipping
|
# this tests various combining characters for anchor alignment and clipping
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
|
"name, text, anchor, direction, epsilon",
|
||||||
|
combine_tests,
|
||||||
|
ids=[r[0] for r in combine_tests],
|
||||||
)
|
)
|
||||||
def test_combine(
|
def test_combine(
|
||||||
name: str, text: str, dir: str | None, anchor: str | None, epsilon: float
|
name: str, text: str, direction: str | None, anchor: str | None, epsilon: float
|
||||||
) -> None:
|
) -> None:
|
||||||
path = f"Tests/images/test_combine_{name}.png"
|
path = f"Tests/images/test_combine_{name}.png"
|
||||||
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
||||||
|
@ -322,11 +309,9 @@ def test_combine(
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.line(((0, 200), (400, 200)), "gray")
|
d.line(((0, 200), (400, 200)), "gray")
|
||||||
d.line(((200, 0), (200, 400)), "gray")
|
d.line(((200, 0), (200, 400)), "gray")
|
||||||
try:
|
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
|
||||||
d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f)
|
|
||||||
except ValueError as ex:
|
|
||||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
|
||||||
pytest.skip("libraqm 0.7 or greater not available")
|
pytest.skip("libraqm 0.7 or greater not available")
|
||||||
|
d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f)
|
||||||
|
|
||||||
assert_image_similar_tofile(im, path, epsilon)
|
assert_image_similar_tofile(im, path, epsilon)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from PIL import Image, ImageMath
|
import pytest
|
||||||
|
|
||||||
|
from PIL import Image, ImageMath, _imagingmath
|
||||||
|
|
||||||
|
|
||||||
def pixel(im: Image.Image | int) -> str | int:
|
def pixel(im: Image.Image | int) -> str | int:
|
||||||
|
@ -498,3 +500,31 @@ def test_logical_not_equal() -> None:
|
||||||
)
|
)
|
||||||
== "I 1"
|
== "I 1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reflected_operands() -> None:
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 + args["A"], **images)) == "I 2"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 - args["A"], **images)) == "I 0"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 * args["A"], **images)) == "I 1"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 / args["A"], **images)) == "I 1"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 % args["A"], **images)) == "I 0"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 ** args["A"], **images)) == "I 1"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 & args["A"], **images)) == "I 1"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 | args["A"], **images)) == "I 1"
|
||||||
|
assert pixel(ImageMath.lambda_eval(lambda args: 1 ^ args["A"], **images)) == "I 0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_mode() -> None:
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
with pytest.raises(ValueError, match="unsupported mode: RGB"):
|
||||||
|
ImageMath.lambda_eval(lambda args: args["im"] + 1, im=im)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_operand_type(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.delattr(_imagingmath, "abs_I")
|
||||||
|
with pytest.raises(TypeError, match="bad operand type for 'abs'"):
|
||||||
|
ImageMath.lambda_eval(lambda args: abs(args["I"]), I=I)
|
||||||
|
|
||||||
|
monkeypatch.delattr(_imagingmath, "max_F")
|
||||||
|
with pytest.raises(TypeError, match="bad operand type for 'max'"):
|
||||||
|
ImageMath.lambda_eval(lambda args: args["max"](args["I"], args["F"]), I=I, F=F)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageMorph, _imagingmorph
|
from PIL import Image, ImageMorph, _imagingmorph
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile, hopper
|
from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind
|
||||||
|
|
||||||
|
|
||||||
def string_to_img(image_string: str) -> Image.Image:
|
def string_to_img(image_string: str) -> Image.Image:
|
||||||
|
@ -266,16 +266,18 @@ def test_unknown_pattern() -> None:
|
||||||
ImageMorph.LutBuilder(op_name="unknown")
|
ImageMorph.LutBuilder(op_name="unknown")
|
||||||
|
|
||||||
|
|
||||||
def test_pattern_syntax_error() -> None:
|
@pytest.mark.parametrize(
|
||||||
|
"pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000)
|
||||||
|
)
|
||||||
|
@timeout_unless_slower_valgrind(1)
|
||||||
|
def test_pattern_syntax_error(pattern: str) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
lb = ImageMorph.LutBuilder(op_name="corner")
|
lb = ImageMorph.LutBuilder(op_name="corner")
|
||||||
new_patterns = ["a pattern with a syntax error"]
|
new_patterns = [pattern]
|
||||||
lb.add_patterns(new_patterns)
|
lb.add_patterns(new_patterns)
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
with pytest.raises(
|
with pytest.raises(Exception, match='Syntax error in pattern "'):
|
||||||
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
|
|
||||||
):
|
|
||||||
lb.build_lut()
|
lb.build_lut()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -57,3 +57,13 @@ def test_constant() -> None:
|
||||||
assert st.rms[0] == 128
|
assert st.rms[0] == 128
|
||||||
assert st.var[0] == 0
|
assert st.var[0] == 0
|
||||||
assert st.stddev[0] == 0
|
assert st.stddev[0] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_zero_count() -> None:
|
||||||
|
im = Image.new("L", (0, 0))
|
||||||
|
|
||||||
|
st = ImageStat.Stat(im)
|
||||||
|
|
||||||
|
assert st.mean == [0]
|
||||||
|
assert st.rms == [0]
|
||||||
|
assert st.var == [0]
|
||||||
|
|
|
@ -28,15 +28,13 @@ def test_numpy_to_image() -> None:
|
||||||
a = numpy.array(data, dtype=dtype)
|
a = numpy.array(data, dtype=dtype)
|
||||||
a.shape = TEST_IMAGE_SIZE
|
a.shape = TEST_IMAGE_SIZE
|
||||||
i = Image.fromarray(a)
|
i = Image.fromarray(a)
|
||||||
if list(i.getdata()) != data:
|
assert list(i.getdata()) == data
|
||||||
print("data mismatch for", dtype)
|
|
||||||
else:
|
else:
|
||||||
data = list(range(100))
|
data = list(range(100))
|
||||||
a = numpy.array([[x] * bands for x in data], dtype=dtype)
|
a = numpy.array([[x] * bands for x in data], dtype=dtype)
|
||||||
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
|
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
|
||||||
i = Image.fromarray(a)
|
i = Image.fromarray(a)
|
||||||
if list(i.getchannel(0).getdata()) != list(range(100)):
|
assert list(i.getchannel(0).getdata()) == list(range(100))
|
||||||
print("data mismatch for", dtype)
|
|
||||||
return i
|
return i
|
||||||
|
|
||||||
# Check supported 1-bit integer formats
|
# Check supported 1-bit integer formats
|
||||||
|
|
|
@ -9,9 +9,30 @@ from PIL import __version__
|
||||||
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
|
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
|
||||||
|
|
||||||
|
|
||||||
|
def map_metadata_keys(md):
|
||||||
|
# Convert installed wheel metadata into canonical Core Metadata 2.4 format.
|
||||||
|
# This was a utility method in pyroma 4.3.3; it was removed in 5.0.
|
||||||
|
# This implementation is constructed from the relevant logic from
|
||||||
|
# Pyroma 5.0's `build_metadata()` implementation. This has been submitted
|
||||||
|
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
|
||||||
|
# so it may be possible to simplify this test in future.
|
||||||
|
data = {}
|
||||||
|
for key in set(md.keys()):
|
||||||
|
value = md.get_all(key)
|
||||||
|
key = pyroma.projectdata.normalize(key)
|
||||||
|
|
||||||
|
if len(value) == 1:
|
||||||
|
value = value[0]
|
||||||
|
if value.strip() == "UNKNOWN":
|
||||||
|
continue
|
||||||
|
|
||||||
|
data[key] = value
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def test_pyroma() -> None:
|
def test_pyroma() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
data = pyroma.projectdata.map_metadata_keys(metadata("Pillow"))
|
data = map_metadata_keys(metadata("Pillow"))
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
rating = pyroma.ratings.rate(data)
|
rating = pyroma.ratings.rate(data)
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageQt
|
from PIL import Image, ImageQt
|
||||||
|
@ -11,18 +8,8 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import PyQt6
|
from pathlib import Path
|
||||||
import PySide6
|
|
||||||
|
|
||||||
QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
|
|
||||||
QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
|
|
||||||
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
|
|
||||||
QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
|
|
||||||
QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
|
|
||||||
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
|
|
||||||
QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
|
|
||||||
QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
|
|
||||||
QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
|
|
||||||
|
|
||||||
if ImageQt.qt_is_installed:
|
if ImageQt.qt_is_installed:
|
||||||
from PIL.ImageQt import QPixmap
|
from PIL.ImageQt import QPixmap
|
||||||
|
@ -32,11 +19,16 @@ if ImageQt.qt_is_installed:
|
||||||
from PyQt6.QtGui import QImage, QPainter, QRegion
|
from PyQt6.QtGui import QImage, QPainter, QRegion
|
||||||
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
|
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
|
||||||
elif ImageQt.qt_version == "side6":
|
elif ImageQt.qt_version == "side6":
|
||||||
from PySide6.QtCore import QPoint
|
from PySide6.QtCore import QPoint # type: ignore[assignment]
|
||||||
from PySide6.QtGui import QImage, QPainter, QRegion
|
from PySide6.QtGui import QImage, QPainter, QRegion # type: ignore[assignment]
|
||||||
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
|
from PySide6.QtWidgets import ( # type: ignore[assignment]
|
||||||
|
QApplication,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
class Example(QWidget): # type: ignore[misc]
|
class Example(QWidget):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
@ -47,9 +39,9 @@ if ImageQt.qt_is_installed:
|
||||||
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
|
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
|
||||||
|
|
||||||
# hbox
|
# hbox
|
||||||
QHBoxLayout(self) # type: ignore[operator]
|
QHBoxLayout(self)
|
||||||
|
|
||||||
lbl = QLabel(self) # type: ignore[operator]
|
lbl = QLabel(self)
|
||||||
# Segfault in the problem
|
# Segfault in the problem
|
||||||
lbl.setPixmap(pixmap1.copy())
|
lbl.setPixmap(pixmap1.copy())
|
||||||
|
|
||||||
|
@ -63,7 +55,7 @@ def roundtrip(expected: Image.Image) -> None:
|
||||||
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
|
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
|
||||||
def test_sanity(tmp_path: Path) -> None:
|
def test_sanity(tmp_path: Path) -> None:
|
||||||
# Segfault test
|
# Segfault test
|
||||||
app: QApplication | None = QApplication([]) # type: ignore[operator]
|
app: QApplication | None = QApplication([])
|
||||||
ex = Example()
|
ex = Example()
|
||||||
assert app # Silence warning
|
assert app # Silence warning
|
||||||
assert ex # Silence warning
|
assert ex # Silence warning
|
||||||
|
@ -84,11 +76,11 @@ def test_sanity(tmp_path: Path) -> None:
|
||||||
imageqt = ImageQt.ImageQt(im)
|
imageqt = ImageQt.ImageQt(im)
|
||||||
data = getattr(QPixmap, "fromImage")(imageqt)
|
data = getattr(QPixmap, "fromImage")(imageqt)
|
||||||
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
|
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
|
||||||
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
|
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32"))
|
||||||
painter = QPainter(qimage) # type: ignore[operator]
|
painter = QPainter(qimage)
|
||||||
image_label = QLabel() # type: ignore[operator]
|
image_label = QLabel()
|
||||||
image_label.setPixmap(data)
|
image_label.setPixmap(data)
|
||||||
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
|
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))
|
||||||
painter.end()
|
painter.end()
|
||||||
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
|
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
|
||||||
qimage.save(rendered_tempfile)
|
qimage.save(rendered_tempfile)
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import ImageQt
|
from PIL import ImageQt
|
||||||
|
|
||||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
pytestmark = pytest.mark.skipif(
|
pytestmark = pytest.mark.skipif(
|
||||||
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
||||||
)
|
)
|
||||||
|
@ -21,7 +23,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||||
src = hopper(mode)
|
src = hopper(mode)
|
||||||
data = ImageQt.toqimage(src)
|
data = ImageQt.toqimage(src)
|
||||||
|
|
||||||
assert isinstance(data, QImage) # type: ignore[arg-type, misc]
|
assert isinstance(data, QImage)
|
||||||
assert not data.isNull()
|
assert not data.isNull()
|
||||||
|
|
||||||
# reload directly from the qimage
|
# reload directly from the qimage
|
||||||
|
|
|
@ -2,14 +2,18 @@ from __future__ import annotations
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
|
||||||
from typing import IO, Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import GifImagePlugin, Image, JpegImagePlugin
|
from PIL import GifImagePlugin, Image, JpegImagePlugin
|
||||||
|
|
||||||
from .helper import cjpeg_available, djpeg_available, is_win32, netpbm_available
|
from .helper import djpeg_available, is_win32, netpbm_available
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
TEST_JPG = "Tests/images/hopper.jpg"
|
TEST_JPG = "Tests/images/hopper.jpg"
|
||||||
TEST_GIF = "Tests/images/hopper.gif"
|
TEST_GIF = "Tests/images/hopper.gif"
|
||||||
|
@ -42,11 +46,6 @@ class TestShellInjection:
|
||||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
im.load_djpeg()
|
im.load_djpeg()
|
||||||
|
|
||||||
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
|
|
||||||
def test_save_cjpeg_filename(self, tmp_path: Path) -> None:
|
|
||||||
with Image.open(TEST_JPG) as im:
|
|
||||||
self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg)
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
||||||
def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None:
|
def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import is_win32
|
|
||||||
|
|
||||||
min_iterations = 100
|
min_iterations = 100
|
||||||
max_iterations = 10000
|
max_iterations = 10000
|
||||||
|
|
||||||
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
pytestmark = pytest.mark.skipif(
|
||||||
|
sys.platform.startswith("win32"), reason="requires Unix or macOS"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_mem_usage() -> float:
|
def _get_mem_usage() -> float:
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, features
|
||||||
|
|
||||||
from .helper import is_win32, skip_unless_feature
|
|
||||||
|
|
||||||
# Limits for testing the leak
|
# Limits for testing the leak
|
||||||
mem_limit = 1024 * 1048576
|
mem_limit = 1024 * 1048576
|
||||||
|
@ -15,8 +14,10 @@ iterations = int((mem_limit / stack_size) * 2)
|
||||||
test_file = "Tests/images/rgb_trns_ycbc.jp2"
|
test_file = "Tests/images/rgb_trns_ycbc.jp2"
|
||||||
|
|
||||||
pytestmark = [
|
pytestmark = [
|
||||||
pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"),
|
pytest.mark.skipif(
|
||||||
skip_unless_feature("jpg_2000"),
|
sys.platform.startswith("win32"), reason="requires Unix or macOS"
|
||||||
|
),
|
||||||
|
pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .helper import hopper, is_win32
|
from PIL import Image
|
||||||
|
|
||||||
iterations = 5000
|
iterations = 5000
|
||||||
|
|
||||||
|
@ -18,7 +19,9 @@ valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
pytestmark = pytest.mark.skipif(
|
||||||
|
sys.platform.startswith("win32"), reason="requires Unix or macOS"
|
||||||
|
)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pre patch:
|
pre patch:
|
||||||
|
@ -112,7 +115,7 @@ standard_chrominance_qtable = (
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
|
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
|
||||||
im = hopper("RGB")
|
with Image.open("Tests/images/hopper.ppm") as im:
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
test_output = BytesIO()
|
test_output = BytesIO()
|
||||||
im.save(test_output, "JPEG", qtables=qtables)
|
im.save(test_output, "JPEG", qtables=qtables)
|
||||||
|
@ -173,9 +176,9 @@ def test_exif_leak() -> None:
|
||||||
0 +----------------------------------------------------------------------->Gi
|
0 +----------------------------------------------------------------------->Gi
|
||||||
0 11.33
|
0 11.33
|
||||||
"""
|
"""
|
||||||
im = hopper("RGB")
|
|
||||||
exif = b"12345678" * 4096
|
exif = b"12345678" * 4096
|
||||||
|
|
||||||
|
with Image.open("Tests/images/hopper.ppm") as im:
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
test_output = BytesIO()
|
test_output = BytesIO()
|
||||||
im.save(test_output, "JPEG", exif=exif)
|
im.save(test_output, "JPEG", exif=exif)
|
||||||
|
@ -207,8 +210,7 @@ def test_base_save() -> None:
|
||||||
| :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@:::
|
| :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@:::
|
||||||
0 +----------------------------------------------------------------------->Gi
|
0 +----------------------------------------------------------------------->Gi
|
||||||
0 7.882"""
|
0 7.882"""
|
||||||
im = hopper("RGB")
|
with Image.open("Tests/images/hopper.ppm") as im:
|
||||||
|
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
test_output = BytesIO()
|
test_output = BytesIO()
|
||||||
im.save(test_output, "JPEG")
|
im.save(test_output, "JPEG")
|
||||||
|
|
|
@ -4,7 +4,6 @@ import platform
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PIL import features
|
from PIL import features
|
||||||
from Tests.helper import is_pypy
|
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_modules() -> None:
|
def test_wheel_modules() -> None:
|
||||||
|
@ -25,8 +24,7 @@ def test_wheel_modules() -> None:
|
||||||
|
|
||||||
elif sys.platform == "ios":
|
elif sys.platform == "ios":
|
||||||
# tkinter is not available on iOS
|
# tkinter is not available on iOS
|
||||||
# libavif is not available on iOS (for now)
|
expected_modules.remove("tkinter")
|
||||||
expected_modules -= {"tkinter", "avif"}
|
|
||||||
|
|
||||||
assert set(features.get_supported_modules()) == expected_modules
|
assert set(features.get_supported_modules()) == expected_modules
|
||||||
|
|
||||||
|
@ -49,8 +47,6 @@ def test_wheel_features() -> None:
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
expected_features.remove("xcb")
|
expected_features.remove("xcb")
|
||||||
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
|
|
||||||
expected_features.remove("zlib_ng")
|
|
||||||
elif sys.platform == "ios":
|
elif sys.platform == "ios":
|
||||||
# Can't distribute raqm due to licensing, and there's no system version;
|
# Can't distribute raqm due to licensing, and there's no system version;
|
||||||
# fribidi and harfbuzz won't be available if raqm isn't available.
|
# fribidi and harfbuzz won't be available if raqm isn't available.
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# install libimagequant
|
# install libimagequant
|
||||||
|
|
||||||
archive_name=libimagequant
|
archive_name=libimagequant
|
||||||
archive_version=4.3.4
|
archive_version=4.4.0
|
||||||
|
|
||||||
archive=$archive_name-$archive_version
|
archive=$archive_name-$archive_version
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# install raqm
|
# install raqm
|
||||||
|
|
||||||
|
|
||||||
archive=libraqm-0.10.2
|
archive=libraqm-0.10.3
|
||||||
|
|
||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install webp
|
# install webp
|
||||||
|
|
||||||
archive=libwebp-1.5.0
|
archive=libwebp-1.6.0
|
||||||
|
|
||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,6 @@ Deprecated features
|
||||||
Below are features which are considered deprecated. Where appropriate,
|
Below are features which are considered deprecated. Where appropriate,
|
||||||
a :py:exc:`DeprecationWarning` is issued.
|
a :py:exc:`DeprecationWarning` is issued.
|
||||||
|
|
||||||
ImageDraw.getdraw hints parameter
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. deprecated:: 10.4.0
|
|
||||||
|
|
||||||
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
|
|
||||||
|
|
||||||
ExifTags.IFD.Makernote
|
ExifTags.IFD.Makernote
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -59,6 +52,23 @@ another mode before saving::
|
||||||
im = Image.new("I", (1, 1))
|
im = Image.new("I", (1, 1))
|
||||||
im.convert("I;16").save("out.png")
|
im.convert("I;16").save("out.png")
|
||||||
|
|
||||||
|
ImageCms.ImageCmsProfile.product_name and .product_info
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. deprecated:: 12.0.0
|
||||||
|
|
||||||
|
``ImageCms.ImageCmsProfile.product_name`` and the corresponding
|
||||||
|
``.product_info`` attributes have been deprecated, and will be removed in
|
||||||
|
Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
|
||||||
|
|
||||||
|
Image._show
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 12.0.0
|
||||||
|
|
||||||
|
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
|
||||||
|
Use :py:meth:`~PIL.ImageShow.show` instead.
|
||||||
|
|
||||||
Removed features
|
Removed features
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
@ -186,6 +196,7 @@ ICNS (width, height, scale) sizes
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. deprecated:: 11.0.0
|
.. deprecated:: 11.0.0
|
||||||
|
.. versionremoved:: 12.0.0
|
||||||
|
|
||||||
Setting an ICNS image size to ``(width, height, scale)`` before loading has been
|
Setting an ICNS image size to ``(width, height, scale)`` before loading has been
|
||||||
removed. Instead, ``load(scale)`` can be used.
|
removed. Instead, ``load(scale)`` can be used.
|
||||||
|
|
|
@ -101,6 +101,28 @@ Palette
|
||||||
The palette mode (``P``) uses a color palette to define the actual color for
|
The palette mode (``P``) uses a color palette to define the actual color for
|
||||||
each pixel.
|
each pixel.
|
||||||
|
|
||||||
|
.. _colors:
|
||||||
|
|
||||||
|
Colors
|
||||||
|
------
|
||||||
|
|
||||||
|
To specify colors, you can use tuples with a value for each channel in the image, e.g.
|
||||||
|
``Image.new("RGB", (1, 1), (255, 0, 0))``.
|
||||||
|
|
||||||
|
If an image has a single channel, you can use a single number instead, e.g.
|
||||||
|
``Image.new("L", (1, 1), 255)``. For "F" mode images, floating point values are also
|
||||||
|
accepted. In the case of "P" mode images, these will be indexes for the color palette.
|
||||||
|
|
||||||
|
If a single value is used for an image with more than one channel, it will still be
|
||||||
|
parsed::
|
||||||
|
|
||||||
|
>>> from PIL import Image
|
||||||
|
>>> im = Image.new("RGBA", (1, 1), 0x04030201)
|
||||||
|
>>> im.getpixel((0, 0))
|
||||||
|
(1, 2, 3, 4)
|
||||||
|
|
||||||
|
Some methods accept other forms, such as color names. See :ref:`color-names`.
|
||||||
|
|
||||||
Info
|
Info
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ Here is a list of PyPI projects that offer additional plugins:
|
||||||
* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
|
* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
|
||||||
* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.
|
* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.
|
||||||
* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images.
|
* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images.
|
||||||
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11.
|
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11.
|
||||||
* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings.
|
* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings.
|
||||||
* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format.
|
* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format.
|
||||||
* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text.
|
* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text.
|
||||||
|
|
|
@ -29,10 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
|
||||||
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
|
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
|
||||||
:alt: GitHub Actions build status (Test MinGW)
|
:alt: GitHub Actions build status (Test MinGW)
|
||||||
|
|
||||||
.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg
|
|
||||||
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
|
|
||||||
:alt: GitHub Actions build status (Test Cygwin)
|
|
||||||
|
|
||||||
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
|
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
|
||||||
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
|
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
|
||||||
:alt: GitHub Actions build status (Wheels)
|
:alt: GitHub Actions build status (Wheels)
|
||||||
|
|
|
@ -44,7 +44,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libtiff** provides compressed TIFF functionality
|
* **libtiff** provides compressed TIFF functionality
|
||||||
|
|
||||||
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0**
|
* Pillow has been tested with libtiff versions **4.0-4.7.0**
|
||||||
|
|
||||||
* **libfreetype** provides type related services
|
* **libfreetype** provides type related services
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libimagequant** provides improved color quantization
|
* **libimagequant** provides improved color quantization
|
||||||
|
|
||||||
* Pillow has been tested with libimagequant **2.6-4.3.4**
|
* Pillow has been tested with libimagequant **2.6-4.4.0**
|
||||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||||
the Pillow license, therefore we will not be distributing binaries
|
the Pillow license, therefore we will not be distributing binaries
|
||||||
with libimagequant support enabled.
|
with libimagequant support enabled.
|
||||||
|
@ -276,10 +276,9 @@ Build options
|
||||||
|
|
||||||
* Config setting: ``-C parallel=n``. Can also be given
|
* Config setting: ``-C parallel=n``. Can also be given
|
||||||
with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
|
with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
|
||||||
multiprocessing to build the extension. Setting ``-C parallel=n``
|
multiprocessing to build the extensions. Setting ``-C parallel=n``
|
||||||
sets the number of CPUs to use to ``n``, or can disable parallel building by
|
sets the number of CPUs to use to ``n``, or can disable parallel building by
|
||||||
using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
|
using a setting of 1. By default, it uses as many CPUs as are present.
|
||||||
available, as many as are present.
|
|
||||||
|
|
||||||
* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
|
* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
|
||||||
``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
|
``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
Python,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
|
Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
|
||||||
Pillow >= 11,Yes,Yes,Yes,Yes,Yes,,,,
|
Pillow 12,Yes,Yes,Yes,Yes,Yes,,,,,
|
||||||
Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,,
|
Pillow 11,,Yes,Yes,Yes,Yes,Yes,,,,
|
||||||
Pillow 10.0,,,Yes,Yes,Yes,Yes,,,
|
Pillow 10.1 - 10.4,,,Yes,Yes,Yes,Yes,Yes,,,
|
||||||
Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,,
|
Pillow 10.0,,,,Yes,Yes,Yes,Yes,,,
|
||||||
Pillow 9.0 - 9.2,,,,Yes,Yes,Yes,Yes,,
|
Pillow 9.3 - 9.5,,,,Yes,Yes,Yes,Yes,Yes,,
|
||||||
Pillow 8.3.2 - 8.4,,,,Yes,Yes,Yes,Yes,Yes,
|
Pillow 9.0 - 9.2,,,,,Yes,Yes,Yes,Yes,,
|
||||||
Pillow 8.0 - 8.3.1,,,,,Yes,Yes,Yes,Yes,
|
Pillow 8.3.2 - 8.4,,,,,Yes,Yes,Yes,Yes,Yes,
|
||||||
Pillow 7.0 - 7.2,,,,,,Yes,Yes,Yes,Yes
|
Pillow 8.0 - 8.3.1,,,,,,Yes,Yes,Yes,Yes,
|
||||||
|
Pillow 7.0 - 7.2,,,,,,,Yes,Yes,Yes,Yes
|
||||||
|
|
|
|
@ -19,45 +19,45 @@ These platforms are built and tested for every change.
|
||||||
+==================================+============================+=====================+
|
+==================================+============================+=====================+
|
||||||
| Alpine | 3.12 | x86-64 |
|
| Alpine | 3.12 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Amazon Linux 2 | 3.9 | x86-64 |
|
| Amazon Linux 2 | 3.10 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Amazon Linux 2023 | 3.9 | x86-64 |
|
| Amazon Linux 2023 | 3.11 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Arch | 3.13 | x86-64 |
|
| Arch | 3.13 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| CentOS Stream 9 | 3.9 | x86-64 |
|
| CentOS Stream 9 | 3.10 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| CentOS Stream 10 | 3.12 | x86-64 |
|
| CentOS Stream 10 | 3.12 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
|
| Debian 13 Trixie | 3.13 | x86, x86-64 |
|
||||||
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Fedora 41 | 3.13 | x86-64 |
|
| Fedora 41 | 3.13 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Fedora 42 | 3.13 | x86-64 |
|
| Fedora 42 | 3.13 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Gentoo | 3.12 | x86-64 |
|
| Gentoo | 3.12 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| macOS 13 Ventura | 3.9 | x86-64 |
|
| macOS 13 Ventura | 3.10 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
|
| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14 | arm64 |
|
||||||
| | 3.14, PyPy3 | |
|
| | PyPy3 | |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
|
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
|
| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
||||||
| | 3.12, 3.13, 3.14, PyPy3 | |
|
| | 3.14, PyPy3 | |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.12 | arm64v8, ppc64le, |
|
| | 3.12 | arm64v8, ppc64le, |
|
||||||
| | | s390x |
|
| | | s390x |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Windows Server 2022 | 3.9 | x86 |
|
| Windows Server 2022 | 3.10 | x86 |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
| | 3.11, 3.12, 3.13, 3.14, | x86-64 |
|
||||||
| | 3.14, PyPy3 | |
|
| | PyPy3 | |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.12 (MinGW) | x86-64 |
|
| | 3.12 (MinGW) | x86-64 |
|
||||||
| +----------------------------+---------------------+
|
|
||||||
| | 3.9 (Cygwin) | x86-64 |
|
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,9 +45,7 @@ Colors
|
||||||
^^^^^^
|
^^^^^^
|
||||||
|
|
||||||
To specify colors, you can use numbers or tuples just as you would use with
|
To specify colors, you can use numbers or tuples just as you would use with
|
||||||
:py:meth:`PIL.Image.new` or :py:meth:`PIL.Image.Image.putpixel`. For “1”,
|
:py:meth:`PIL.Image.new`. See :ref:`colors` for more information.
|
||||||
“L”, and “I” images, use integers. For “RGB” images, use a 3-tuple containing
|
|
||||||
integer values. For “F” images, use integer or floating point values.
|
|
||||||
|
|
||||||
For palette images (mode “P”), use integers as color indexes. In 1.1.4 and
|
For palette images (mode “P”), use integers as color indexes. In 1.1.4 and
|
||||||
later, you can also use RGB 3-tuples or color names (see below). The drawing
|
later, you can also use RGB 3-tuples or color names (see below). The drawing
|
||||||
|
|
|
@ -74,5 +74,6 @@ Constants
|
||||||
---------
|
---------
|
||||||
|
|
||||||
.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES
|
.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES
|
||||||
|
.. autodata:: PIL.ImageFile.MAXBLOCK
|
||||||
.. autodata:: PIL.ImageFile.ERRORS
|
.. autodata:: PIL.ImageFile.ERRORS
|
||||||
:annotation:
|
:annotation:
|
||||||
|
|
|
@ -59,7 +59,7 @@ Access using negative indexes is also possible. ::
|
||||||
|
|
||||||
Modifies the pixel at x,y. The color is given as a single
|
Modifies the pixel at x,y. The color is given as a single
|
||||||
numerical value for single band images, and a tuple for
|
numerical value for single band images, and a tuple for
|
||||||
multi-band images.
|
multi-band images. See :ref:`colors` for more information.
|
||||||
|
|
||||||
:param xy: The pixel coordinate, given as (x, y).
|
:param xy: The pixel coordinate, given as (x, y).
|
||||||
:param color: The pixel value according to its mode,
|
:param color: The pixel value according to its mode,
|
||||||
|
|
|
@ -53,11 +53,6 @@ on some Python versions.
|
||||||
|
|
||||||
An object that supports the read method.
|
An object that supports the read method.
|
||||||
|
|
||||||
.. py:data:: TypeGuard
|
|
||||||
:value: typing.TypeGuard
|
|
||||||
|
|
||||||
See :py:obj:`typing.TypeGuard`.
|
|
||||||
|
|
||||||
:mod:`~PIL._util` module
|
:mod:`~PIL._util` module
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,12 @@ TODO
|
||||||
Backwards incompatible changes
|
Backwards incompatible changes
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
Python 3.9
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
Pillow has dropped support for Python 3.9,
|
||||||
|
which reached end-of-life in October 2025.
|
||||||
|
|
||||||
ImageFile.raise_oserror
|
ImageFile.raise_oserror
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -110,10 +116,18 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
|
||||||
Deprecations
|
Deprecations
|
||||||
============
|
============
|
||||||
|
|
||||||
TODO
|
Image._show
|
||||||
^^^^
|
^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
|
||||||
|
Use :py:meth:`~PIL.ImageShow.show` instead.
|
||||||
|
|
||||||
|
ImageCms.ImageCmsProfile.product_name and .product_info
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
``ImageCms.ImageCmsProfile.product_name`` and the corresponding
|
||||||
|
``.product_info`` attributes have been deprecated, and will be removed in
|
||||||
|
Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
|
||||||
|
|
||||||
API changes
|
API changes
|
||||||
===========
|
===========
|
||||||
|
@ -134,7 +148,18 @@ TODO
|
||||||
Other changes
|
Other changes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
TODO
|
Python 3.14
|
||||||
^^^^
|
^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
Pillow 11.3.0 had wheels built against Python 3.14 beta, available as a preview to help
|
||||||
|
others prepare for 3.14, and to ensure Pillow could be used immediately at the release
|
||||||
|
of 3.14.0 final (2025-10-07, :pep:`745`).
|
||||||
|
|
||||||
|
Pillow 12.0.0 now officially supports Python 3.14.
|
||||||
|
|
||||||
|
ImageMorph operations must have length 1
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character
|
||||||
|
within Pillow, long execution times can be avoided if a user provided long pattern
|
||||||
|
strings. Reported by Jang Choi.
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
# libwebp example binaries require dependencies that aren't available for iOS builds.
|
|
||||||
# There's also no easy way to invoke the build to *exclude* the example builds.
|
|
||||||
# Since we don't need the examples anyway, remove them from the Makefile.
|
|
||||||
#
|
|
||||||
# As a point of reference, libwebp provides an XCFramework build script that involves
|
|
||||||
# 7 separate invocations of make to avoid building the examples. Patching the Makefile
|
|
||||||
# to remove the examples is a simpler approach, and one that is more compatible with
|
|
||||||
# the existing multibuild infrastructure.
|
|
||||||
#
|
|
||||||
# In the next release, it should be possible to pass --disable-libwebpexamples
|
|
||||||
# instead of applying this patch.
|
|
||||||
#
|
|
||||||
diff -ur libwebp-1.5.0-orig/Makefile.am libwebp-1.5.0/Makefile.am
|
|
||||||
--- libwebp-1.5.0-orig/Makefile.am 2024-12-20 09:17:50
|
|
||||||
+++ libwebp-1.5.0/Makefile.am 2025-01-09 11:24:17
|
|
||||||
@@ -5,5 +5,3 @@
|
|
||||||
if BUILD_EXTRAS
|
|
||||||
SUBDIRS += extras
|
|
||||||
endif
|
|
||||||
-
|
|
||||||
-SUBDIRS += examples
|
|
||||||
diff -ur libwebp-1.5.0-orig/Makefile.in libwebp-1.5.0/Makefile.in
|
|
||||||
--- libwebp-1.5.0-orig/Makefile.in 2024-12-20 09:52:53
|
|
||||||
+++ libwebp-1.5.0/Makefile.in 2025-01-09 11:24:17
|
|
||||||
@@ -156,7 +156,7 @@
|
|
||||||
unique=`for i in $$list; do \
|
|
||||||
if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
|
|
||||||
done | $(am__uniquify_input)`
|
|
||||||
-DIST_SUBDIRS = sharpyuv src imageio man extras examples
|
|
||||||
+DIST_SUBDIRS = sharpyuv src imageio man extras
|
|
||||||
am__DIST_COMMON = $(srcdir)/Makefile.in \
|
|
||||||
$(top_srcdir)/src/webp/config.h.in AUTHORS COPYING ChangeLog \
|
|
||||||
NEWS README.md ar-lib compile config.guess config.sub \
|
|
||||||
@@ -351,7 +351,7 @@
|
|
||||||
top_srcdir = @top_srcdir@
|
|
||||||
webp_libname_prefix = @webp_libname_prefix@
|
|
||||||
ACLOCAL_AMFLAGS = -I m4
|
|
||||||
-SUBDIRS = sharpyuv src imageio man $(am__append_1) examples
|
|
||||||
+SUBDIRS = sharpyuv src imageio man $(am__append_1)
|
|
||||||
EXTRA_DIST = COPYING autogen.sh
|
|
||||||
all: all-recursive
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[build-system]
|
[build-system]
|
||||||
build-backend = "backend"
|
build-backend = "backend"
|
||||||
requires = [
|
requires = [
|
||||||
|
"pybind11",
|
||||||
"setuptools>=77",
|
"setuptools>=77",
|
||||||
]
|
]
|
||||||
backend-path = [
|
backend-path = [
|
||||||
|
@ -19,15 +20,15 @@ license-files = [ "LICENSE" ]
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
|
{ name = "Jeffrey A. Clark", email = "aclark@aclark.net" },
|
||||||
]
|
]
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 6 - Mature",
|
"Development Status :: 6 - Mature",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Topic :: Multimedia :: Graphics",
|
"Topic :: Multimedia :: Graphics",
|
||||||
|
@ -66,7 +67,7 @@ optional-dependencies.tests = [
|
||||||
"markdown2",
|
"markdown2",
|
||||||
"olefile",
|
"olefile",
|
||||||
"packaging",
|
"packaging",
|
||||||
"pyroma",
|
"pyroma>=5",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-timeout",
|
"pytest-timeout",
|
||||||
|
@ -74,9 +75,6 @@ optional-dependencies.tests = [
|
||||||
"trove-classifiers>=2024.10.12",
|
"trove-classifiers>=2024.10.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
optional-dependencies.typing = [
|
|
||||||
"typing-extensions; python_version<'3.10'",
|
|
||||||
]
|
|
||||||
optional-dependencies.xmp = [
|
optional-dependencies.xmp = [
|
||||||
"defusedxml",
|
"defusedxml",
|
||||||
]
|
]
|
||||||
|
@ -187,8 +185,8 @@ lint.ignore = [
|
||||||
"PT011", # pytest-raises-too-broad
|
"PT011", # pytest-raises-too-broad
|
||||||
"PT012", # pytest-raises-with-multiple-statements
|
"PT012", # pytest-raises-with-multiple-statements
|
||||||
"PT017", # pytest-assert-in-except
|
"PT017", # pytest-assert-in-except
|
||||||
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
|
|
||||||
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
|
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
|
||||||
|
"UP038", # pyupgrade: deprecated rule
|
||||||
]
|
]
|
||||||
lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
|
lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
|
||||||
"I002",
|
"I002",
|
||||||
|
@ -205,7 +203,7 @@ lint.isort.required-imports = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pyproject-fmt]
|
[tool.pyproject-fmt]
|
||||||
max_supported_python = "3.13"
|
max_supported_python = "3.14"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-ra --color=auto"
|
addopts = "-ra --color=auto"
|
||||||
|
@ -214,7 +212,7 @@ testpaths = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.9"
|
python_version = "3.10"
|
||||||
pretty = true
|
pretty = true
|
||||||
disallow_any_generics = true
|
disallow_any_generics = true
|
||||||
enable_error_code = "ignore-without-code"
|
enable_error_code = "ignore-without-code"
|
||||||
|
|
23
setup.py
23
setup.py
|
@ -17,9 +17,20 @@ import sys
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from pybind11.setup_helpers import ParallelCompile
|
||||||
from setuptools import Extension, setup
|
from setuptools import Extension, setup
|
||||||
from setuptools.command.build_ext import build_ext
|
from setuptools.command.build_ext import build_ext
|
||||||
|
|
||||||
|
configuration: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
# parse configuration from _custom_build/backend.py
|
||||||
|
while sys.argv[-1].startswith("--pillow-configuration="):
|
||||||
|
_, key, value = sys.argv.pop().split("=", 2)
|
||||||
|
configuration.setdefault(key, []).append(value)
|
||||||
|
|
||||||
|
default = int(configuration.get("parallel", ["0"])[-1])
|
||||||
|
ParallelCompile("MAX_CONCURRENCY", default).install()
|
||||||
|
|
||||||
|
|
||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
version_file = "src/PIL/_version.py"
|
version_file = "src/PIL/_version.py"
|
||||||
|
@ -27,9 +38,6 @@ def get_version() -> str:
|
||||||
return f.read().split('"')[1]
|
return f.read().split('"')[1]
|
||||||
|
|
||||||
|
|
||||||
configuration: dict[str, list[str]] = {}
|
|
||||||
|
|
||||||
|
|
||||||
PILLOW_VERSION = get_version()
|
PILLOW_VERSION = get_version()
|
||||||
AVIF_ROOT = None
|
AVIF_ROOT = None
|
||||||
FREETYPE_ROOT = None
|
FREETYPE_ROOT = None
|
||||||
|
@ -386,9 +394,7 @@ class pil_build_ext(build_ext):
|
||||||
cpu_count = os.cpu_count()
|
cpu_count = os.cpu_count()
|
||||||
if cpu_count is not None:
|
if cpu_count is not None:
|
||||||
try:
|
try:
|
||||||
self.parallel = int(
|
self.parallel = int(os.environ.get("MAX_CONCURRENCY", cpu_count))
|
||||||
os.environ.get("MAX_CONCURRENCY", min(4, cpu_count))
|
|
||||||
)
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
for x in self.feature:
|
for x in self.feature:
|
||||||
|
@ -1083,11 +1089,6 @@ ext_modules = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# parse configuration from _custom_build/backend.py
|
|
||||||
while sys.argv[-1].startswith("--pillow-configuration="):
|
|
||||||
_, key, value = sys.argv.pop().split("=", 2)
|
|
||||||
configuration.setdefault(key, []).append(value)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setup(
|
setup(
|
||||||
cmdclass={"build_ext": pil_build_ext},
|
cmdclass={"build_ext": pil_build_ext},
|
||||||
|
|
|
@ -30,7 +30,7 @@ from ._util import DeferredError
|
||||||
|
|
||||||
def _accept(prefix: bytes) -> bool:
|
def _accept(prefix: bytes) -> bool:
|
||||||
return (
|
return (
|
||||||
len(prefix) >= 6
|
len(prefix) >= 16
|
||||||
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||||
and i16(prefix, 14) in [0, 3] # flags
|
and i16(prefix, 14) in [0, 3] # flags
|
||||||
)
|
)
|
||||||
|
|
|
@ -54,7 +54,7 @@ class GbrImageFile(ImageFile.ImageFile):
|
||||||
width = i32(self.fp.read(4))
|
width = i32(self.fp.read(4))
|
||||||
height = i32(self.fp.read(4))
|
height = i32(self.fp.read(4))
|
||||||
color_depth = i32(self.fp.read(4))
|
color_depth = i32(self.fp.read(4))
|
||||||
if width <= 0 or height <= 0:
|
if width == 0 or height == 0:
|
||||||
msg = "not a GIMP brush"
|
msg = "not a GIMP brush"
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
if color_depth not in (1, 4):
|
if color_depth not in (1, 4):
|
||||||
|
@ -71,7 +71,7 @@ class GbrImageFile(ImageFile.ImageFile):
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
self.info["spacing"] = i32(self.fp.read(4))
|
self.info["spacing"] = i32(self.fp.read(4))
|
||||||
|
|
||||||
comment = self.fp.read(comment_length)[:-1]
|
self.info["comment"] = self.fp.read(comment_length)[:-1]
|
||||||
|
|
||||||
if color_depth == 1:
|
if color_depth == 1:
|
||||||
self._mode = "L"
|
self._mode = "L"
|
||||||
|
@ -80,8 +80,6 @@ class GbrImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
self._size = width, height
|
self._size = width, height
|
||||||
|
|
||||||
self.info["comment"] = comment
|
|
||||||
|
|
||||||
# Image might not be small
|
# Image might not be small
|
||||||
Image._decompression_bomb_check(self.size)
|
Image._decompression_bomb_check(self.size)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import IO, Any, Literal, NamedTuple, Union, cast
|
from typing import Any, NamedTuple, cast
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
Image,
|
Image,
|
||||||
|
@ -49,6 +49,8 @@ from ._util import DeferredError
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from typing import IO, Literal
|
||||||
|
|
||||||
from . import _imaging
|
from . import _imaging
|
||||||
from ._typing import Buffer
|
from ._typing import Buffer
|
||||||
|
|
||||||
|
@ -535,7 +537,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
|
||||||
return im.convert("L")
|
return im.convert("L")
|
||||||
|
|
||||||
|
|
||||||
_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]
|
_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette
|
||||||
|
|
||||||
|
|
||||||
def _normalize_palette(
|
def _normalize_palette(
|
||||||
|
|
|
@ -21,10 +21,14 @@ See the GIMP distribution for more information.)
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from math import log, pi, sin, sqrt
|
from math import log, pi, sin, sqrt
|
||||||
from typing import IO, Callable
|
|
||||||
|
|
||||||
from ._binary import o8
|
from ._binary import o8
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
EPSILON = 1e-10
|
EPSILON = 1e-10
|
||||||
"""""" # Enable auto-doc for data member
|
"""""" # Enable auto-doc for data member
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix: bytes) -> bool:
|
def _accept(prefix: bytes) -> bool:
|
||||||
return prefix.startswith(b"GRIB") and prefix[7] == 1
|
return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
|
||||||
|
|
||||||
|
|
||||||
class GribStubImageFile(ImageFile.StubImageFile):
|
class GribStubImageFile(ImageFile.StubImageFile):
|
||||||
|
|
|
@ -38,10 +38,9 @@ import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Callable, Iterator, MutableMapping, Sequence
|
from collections.abc import MutableMapping
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from types import ModuleType
|
from typing import IO, Protocol, cast
|
||||||
from typing import IO, Any, Literal, Protocol, cast
|
|
||||||
|
|
||||||
# VERSION was removed in Pillow 6.0.0.
|
# VERSION was removed in Pillow 6.0.0.
|
||||||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||||
|
@ -64,6 +63,12 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ElementTree = None
|
ElementTree = None
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable, Iterator, Sequence
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,7 +103,6 @@ try:
|
||||||
raise ImportError(msg)
|
raise ImportError(msg)
|
||||||
|
|
||||||
except ImportError as v:
|
except ImportError as v:
|
||||||
core = DeferredError.new(ImportError("The _imaging C module is not installed."))
|
|
||||||
# Explanations for ways that we know we might have an import error
|
# Explanations for ways that we know we might have an import error
|
||||||
if str(v).startswith("Module use of python"):
|
if str(v).startswith("Module use of python"):
|
||||||
# The _imaging C module is present, but not compiled for
|
# The _imaging C module is present, but not compiled for
|
||||||
|
@ -1732,7 +1736,8 @@ class Image:
|
||||||
Instead of an image, the source can be a integer or tuple
|
Instead of an image, the source can be a integer or tuple
|
||||||
containing pixel values. The method then fills the region
|
containing pixel values. The method then fills the region
|
||||||
with the given color. When creating RGB images, you can
|
with the given color. When creating RGB images, you can
|
||||||
also use color strings as supported by the ImageColor module.
|
also use color strings as supported by the ImageColor module. See
|
||||||
|
:ref:`colors` for more information.
|
||||||
|
|
||||||
If a mask is given, this method updates only the regions
|
If a mask is given, this method updates only the regions
|
||||||
indicated by the mask. You can use either "1", "L", "LA", "RGBA"
|
indicated by the mask. You can use either "1", "L", "LA", "RGBA"
|
||||||
|
@ -1988,7 +1993,8 @@ class Image:
|
||||||
sequence ends. The scale and offset values are used to adjust the
|
sequence ends. The scale and offset values are used to adjust the
|
||||||
sequence values: **pixel = value*scale + offset**.
|
sequence values: **pixel = value*scale + offset**.
|
||||||
|
|
||||||
:param data: A flattened sequence object.
|
:param data: A flattened sequence object. See :ref:`colors` for more
|
||||||
|
information about values.
|
||||||
:param scale: An optional scale value. The default is 1.0.
|
:param scale: An optional scale value. The default is 1.0.
|
||||||
:param offset: An optional offset value. The default is 0.0.
|
:param offset: An optional offset value. The default is 0.0.
|
||||||
"""
|
"""
|
||||||
|
@ -2047,7 +2053,7 @@ class Image:
|
||||||
Modifies the pixel at the given position. The color is given as
|
Modifies the pixel at the given position. The color is given as
|
||||||
a single numerical value for single-band images, and a tuple for
|
a single numerical value for single-band images, and a tuple for
|
||||||
multi-band images. In addition to this, RGB and RGBA tuples are
|
multi-band images. In addition to this, RGB and RGBA tuples are
|
||||||
accepted for P and PA images.
|
accepted for P and PA images. See :ref:`colors` for more information.
|
||||||
|
|
||||||
Note that this method is relatively slow. For more extensive changes,
|
Note that this method is relatively slow. For more extensive changes,
|
||||||
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
|
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
|
||||||
|
@ -2694,7 +2700,9 @@ class Image:
|
||||||
:param title: Optional title to use for the image window, where possible.
|
:param title: Optional title to use for the image window, where possible.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_show(self, title=title)
|
from . import ImageShow
|
||||||
|
|
||||||
|
ImageShow.show(self, title)
|
||||||
|
|
||||||
def split(self) -> tuple[Image, ...]:
|
def split(self) -> tuple[Image, ...]:
|
||||||
"""
|
"""
|
||||||
|
@ -3123,12 +3131,12 @@ def new(
|
||||||
:param mode: The mode to use for the new image. See:
|
:param mode: The mode to use for the new image. See:
|
||||||
:ref:`concept-modes`.
|
:ref:`concept-modes`.
|
||||||
:param size: A 2-tuple, containing (width, height) in pixels.
|
:param size: A 2-tuple, containing (width, height) in pixels.
|
||||||
:param color: What color to use for the image. Default is black.
|
:param color: What color to use for the image. Default is black. If given,
|
||||||
If given, this should be a single integer or floating point value
|
this should be a single integer or floating point value for single-band
|
||||||
for single-band modes, and a tuple for multi-band modes (one value
|
modes, and a tuple for multi-band modes (one value per band). When
|
||||||
per band). When creating RGB or HSV images, you can also use color
|
creating RGB or HSV images, you can also use color strings as supported
|
||||||
strings as supported by the ImageColor module. If the color is
|
by the ImageColor module. See :ref:`colors` for more information. If the
|
||||||
None, the image is not initialised.
|
color is None, the image is not initialised.
|
||||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -3632,9 +3640,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image:
|
||||||
"""
|
"""
|
||||||
Alpha composite im2 over im1.
|
Alpha composite im2 over im1.
|
||||||
|
|
||||||
:param im1: The first image. Must have mode RGBA.
|
:param im1: The first image. Must have mode RGBA or LA.
|
||||||
:param im2: The second image. Must have mode RGBA, and the same size as
|
:param im2: The second image. Must have the same mode and size as the first image.
|
||||||
the first image.
|
|
||||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -3860,6 +3867,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
|
||||||
def _show(image: Image, **options: Any) -> None:
|
def _show(image: Image, **options: Any) -> None:
|
||||||
from . import ImageShow
|
from . import ImageShow
|
||||||
|
|
||||||
|
deprecate("Image._show", 13, "ImageShow.show")
|
||||||
ImageShow.show(image, **options)
|
ImageShow.show(image, **options)
|
||||||
|
|
||||||
|
|
||||||
|
@ -4281,6 +4289,8 @@ class Exif(_ExifBase):
|
||||||
del self._info[tag]
|
del self._info[tag]
|
||||||
else:
|
else:
|
||||||
del self._data[tag]
|
del self._data[tag]
|
||||||
|
if tag in self._ifds:
|
||||||
|
del self._ifds[tag]
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[int]:
|
def __iter__(self) -> Iterator[int]:
|
||||||
keys = set(self._data)
|
keys = set(self._data)
|
||||||
|
|
|
@ -23,9 +23,10 @@ import operator
|
||||||
import sys
|
import sys
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import Literal, SupportsFloat, SupportsInt, Union
|
from typing import Any, Literal, SupportsFloat, SupportsInt, Union
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
|
from ._deprecate import deprecate
|
||||||
from ._typing import SupportsRead
|
from ._typing import SupportsRead
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -233,9 +234,7 @@ class ImageCmsProfile:
|
||||||
low-level profile object
|
low-level profile object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.filename = None
|
self.filename: str | None = None
|
||||||
self.product_name = None # profile.product_name
|
|
||||||
self.product_info = None # profile.product_info
|
|
||||||
|
|
||||||
if isinstance(profile, str):
|
if isinstance(profile, str):
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
@ -256,6 +255,13 @@ class ImageCmsProfile:
|
||||||
msg = "Invalid type for Profile" # type: ignore[unreachable]
|
msg = "Invalid type for Profile" # type: ignore[unreachable]
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
if name in ("product_name", "product_info"):
|
||||||
|
deprecate(f"ImageCms.ImageCmsProfile.{name}", 13)
|
||||||
|
return None
|
||||||
|
msg = f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
||||||
|
raise AttributeError(msg)
|
||||||
|
|
||||||
def tobytes(self) -> bytes:
|
def tobytes(self) -> bytes:
|
||||||
"""
|
"""
|
||||||
Returns the profile in a format suitable for embedding in
|
Returns the profile in a format suitable for embedding in
|
||||||
|
|
|
@ -34,20 +34,23 @@ from __future__ import annotations
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from types import ModuleType
|
from typing import cast
|
||||||
from typing import Any, AnyStr, Callable, Union, cast
|
|
||||||
|
|
||||||
from . import Image, ImageColor
|
from . import Image, ImageColor
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any, AnyStr
|
||||||
|
|
||||||
|
from . import ImageDraw2, ImageFont
|
||||||
from ._typing import Coords
|
from ._typing import Coords
|
||||||
|
|
||||||
# experimental access to the outline API
|
# experimental access to the outline API
|
||||||
Outline: Callable[[], Image.core._Outline] = Image.core.outline
|
Outline: Callable[[], Image.core._Outline] = Image.core.outline
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
_Ink = float | tuple[int, ...] | str
|
||||||
if TYPE_CHECKING:
|
|
||||||
from . import ImageDraw2, ImageFont
|
|
||||||
|
|
||||||
_Ink = Union[float, tuple[int, ...], str]
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A simple 2D drawing interface for PIL images.
|
A simple 2D drawing interface for PIL images.
|
||||||
|
|
|
@ -46,6 +46,18 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAXBLOCK = 65536
|
MAXBLOCK = 65536
|
||||||
|
"""
|
||||||
|
By default, Pillow processes image data in blocks. This helps to prevent excessive use
|
||||||
|
of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``.
|
||||||
|
|
||||||
|
When reading an image, this is the number of bytes to read at once.
|
||||||
|
|
||||||
|
When writing an image, this is the number of bytes to write at once.
|
||||||
|
If the image width times 4 is greater, then that will be used instead.
|
||||||
|
Plugins may also set a greater number.
|
||||||
|
|
||||||
|
User code may set this to another number.
|
||||||
|
"""
|
||||||
|
|
||||||
SAFEBLOCK = 1024 * 1024
|
SAFEBLOCK = 1024 * 1024
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,14 @@ from __future__ import annotations
|
||||||
import abc
|
import abc
|
||||||
import functools
|
import functools
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from types import ModuleType
|
from typing import cast
|
||||||
from typing import Any, Callable, cast
|
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from . import _imaging
|
from . import _imaging
|
||||||
from ._typing import NumpyArray
|
from ._typing import NumpyArray
|
||||||
|
|
||||||
|
|
|
@ -671,11 +671,7 @@ class FreeTypeFont:
|
||||||
:returns: A list of the named styles in a variation font.
|
:returns: A list of the named styles in a variation font.
|
||||||
:exception OSError: If the font is not a variation font.
|
:exception OSError: If the font is not a variation font.
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
names = self.font.getvarnames()
|
names = self.font.getvarnames()
|
||||||
except AttributeError as e:
|
|
||||||
msg = "FreeType 2.9.1 or greater is required"
|
|
||||||
raise NotImplementedError(msg) from e
|
|
||||||
return [name.replace(b"\x00", b"") for name in names]
|
return [name.replace(b"\x00", b"") for name in names]
|
||||||
|
|
||||||
def set_variation_by_name(self, name: str | bytes) -> None:
|
def set_variation_by_name(self, name: str | bytes) -> None:
|
||||||
|
@ -702,11 +698,7 @@ class FreeTypeFont:
|
||||||
:returns: A list of the axes in a variation font.
|
:returns: A list of the axes in a variation font.
|
||||||
:exception OSError: If the font is not a variation font.
|
:exception OSError: If the font is not a variation font.
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
axes = self.font.getvaraxes()
|
axes = self.font.getvaraxes()
|
||||||
except AttributeError as e:
|
|
||||||
msg = "FreeType 2.9.1 or greater is required"
|
|
||||||
raise NotImplementedError(msg) from e
|
|
||||||
for axis in axes:
|
for axis in axes:
|
||||||
if axis["name"]:
|
if axis["name"]:
|
||||||
axis["name"] = axis["name"].replace(b"\x00", b"")
|
axis["name"] = axis["name"].replace(b"\x00", b"")
|
||||||
|
@ -717,11 +709,7 @@ class FreeTypeFont:
|
||||||
:param axes: A list of values for each axis.
|
:param axes: A list of values for each axis.
|
||||||
:exception OSError: If the font is not a variation font.
|
:exception OSError: If the font is not a variation font.
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
self.font.setvaraxes(axes)
|
self.font.setvaraxes(axes)
|
||||||
except AttributeError as e:
|
|
||||||
msg = "FreeType 2.9.1 or greater is required"
|
|
||||||
raise NotImplementedError(msg) from e
|
|
||||||
|
|
||||||
|
|
||||||
class TransposedFont:
|
class TransposedFont:
|
||||||
|
|
|
@ -17,11 +17,15 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
from types import CodeType
|
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
from . import Image, _imagingmath
|
from . import Image, _imagingmath
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from types import CodeType
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class _Operand:
|
class _Operand:
|
||||||
"""Wraps an image operand, providing standard operators"""
|
"""Wraps an image operand, providing standard operators"""
|
||||||
|
|
|
@ -150,7 +150,7 @@ class LutBuilder:
|
||||||
|
|
||||||
# Parse and create symmetries of the patterns strings
|
# Parse and create symmetries of the patterns strings
|
||||||
for p in self.patterns:
|
for p in self.patterns:
|
||||||
m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
|
m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
|
||||||
if not m:
|
if not m:
|
||||||
msg = 'Syntax error in pattern "' + p + '"'
|
msg = 'Syntax error in pattern "' + p + '"'
|
||||||
raise Exception(msg)
|
raise Exception(msg)
|
||||||
|
|
|
@ -19,23 +19,18 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Callable, Union
|
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
from ._util import is_path
|
from ._util import is_path
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import PyQt6
|
from collections.abc import Callable
|
||||||
import PySide6
|
from typing import Any
|
||||||
|
|
||||||
from . import ImageFile
|
from . import ImageFile
|
||||||
|
|
||||||
QBuffer: type
|
QBuffer: type
|
||||||
QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray]
|
|
||||||
QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice]
|
|
||||||
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
|
|
||||||
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
|
|
||||||
|
|
||||||
qt_version: str | None
|
qt_version: str | None
|
||||||
qt_versions = [
|
qt_versions = [
|
||||||
|
@ -49,11 +44,15 @@ for version, qt_module in qt_versions:
|
||||||
try:
|
try:
|
||||||
qRgba: Callable[[int, int, int, int], int]
|
qRgba: Callable[[int, int, int, int], int]
|
||||||
if qt_module == "PyQt6":
|
if qt_module == "PyQt6":
|
||||||
from PyQt6.QtCore import QBuffer, QIODevice
|
from PyQt6.QtCore import QBuffer, QByteArray, QIODevice
|
||||||
from PyQt6.QtGui import QImage, QPixmap, qRgba
|
from PyQt6.QtGui import QImage, QPixmap, qRgba
|
||||||
elif qt_module == "PySide6":
|
elif qt_module == "PySide6":
|
||||||
from PySide6.QtCore import QBuffer, QIODevice
|
from PySide6.QtCore import ( # type: ignore[assignment]
|
||||||
from PySide6.QtGui import QImage, QPixmap, qRgba
|
QBuffer,
|
||||||
|
QByteArray,
|
||||||
|
QIODevice,
|
||||||
|
)
|
||||||
|
from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment]
|
||||||
except (ImportError, RuntimeError):
|
except (ImportError, RuntimeError):
|
||||||
continue
|
continue
|
||||||
qt_is_installed = True
|
qt_is_installed = True
|
||||||
|
@ -183,7 +182,7 @@ def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
|
||||||
|
|
||||||
if qt_is_installed:
|
if qt_is_installed:
|
||||||
|
|
||||||
class ImageQt(QImage): # type: ignore[misc]
|
class ImageQt(QImage):
|
||||||
def __init__(self, im: Image.Image | str | QByteArray) -> None:
|
def __init__(self, im: Image.Image | str | QByteArray) -> None:
|
||||||
"""
|
"""
|
||||||
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
|
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
##
|
##
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|
||||||
class Iterator:
|
class Iterator:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -120,7 +120,7 @@ class Stat:
|
||||||
@cached_property
|
@cached_property
|
||||||
def mean(self) -> list[float]:
|
def mean(self) -> list[float]:
|
||||||
"""Average (arithmetic mean) pixel level for each band in the image."""
|
"""Average (arithmetic mean) pixel level for each band in the image."""
|
||||||
return [self.sum[i] / self.count[i] for i in self.bands]
|
return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def median(self) -> list[int]:
|
def median(self) -> list[int]:
|
||||||
|
@ -141,13 +141,20 @@ class Stat:
|
||||||
@cached_property
|
@cached_property
|
||||||
def rms(self) -> list[float]:
|
def rms(self) -> list[float]:
|
||||||
"""RMS (root-mean-square) for each band in the image."""
|
"""RMS (root-mean-square) for each band in the image."""
|
||||||
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
|
return [
|
||||||
|
math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
|
||||||
|
for i in self.bands
|
||||||
|
]
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def var(self) -> list[float]:
|
def var(self) -> list[float]:
|
||||||
"""Variance for each band in the image."""
|
"""Variance for each band in the image."""
|
||||||
return [
|
return [
|
||||||
|
(
|
||||||
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
|
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
|
||||||
|
if self.count[i]
|
||||||
|
else 0
|
||||||
|
)
|
||||||
for i in self.bands
|
for i in self.bands
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -34,10 +34,6 @@ def _i(c: bytes) -> int:
|
||||||
return i32((b"\0\0\0\0" + c)[-4:])
|
return i32((b"\0\0\0\0" + c)[-4:])
|
||||||
|
|
||||||
|
|
||||||
def _i8(c: int | bytes) -> int:
|
|
||||||
return c if isinstance(c, int) else c[0]
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
|
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
|
||||||
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
|
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
|
||||||
|
@ -100,16 +96,18 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
# mode
|
# mode
|
||||||
layers = self.info[(3, 60)][0]
|
layers = self.info[(3, 60)][0]
|
||||||
component = self.info[(3, 60)][1]
|
component = self.info[(3, 60)][1]
|
||||||
if (3, 65) in self.info:
|
|
||||||
id = self.info[(3, 65)][0] - 1
|
|
||||||
else:
|
|
||||||
id = 0
|
|
||||||
if layers == 1 and not component:
|
if layers == 1 and not component:
|
||||||
self._mode = "L"
|
self._mode = "L"
|
||||||
elif layers == 3 and component:
|
band = None
|
||||||
self._mode = "RGB"[id]
|
else:
|
||||||
|
if layers == 3 and component:
|
||||||
|
self._mode = "RGB"
|
||||||
elif layers == 4 and component:
|
elif layers == 4 and component:
|
||||||
self._mode = "CMYK"[id]
|
self._mode = "CMYK"
|
||||||
|
if (3, 65) in self.info:
|
||||||
|
band = self.info[(3, 65)][0] - 1
|
||||||
|
else:
|
||||||
|
band = 0
|
||||||
|
|
||||||
# size
|
# size
|
||||||
self._size = self.getint((3, 20)), self.getint((3, 30))
|
self._size = self.getint((3, 20)), self.getint((3, 30))
|
||||||
|
@ -124,16 +122,16 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
# tile
|
# tile
|
||||||
if tag == (8, 10):
|
if tag == (8, 10):
|
||||||
self.tile = [
|
self.tile = [
|
||||||
ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
|
ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
|
||||||
]
|
]
|
||||||
|
|
||||||
def load(self) -> Image.core.PixelAccess | None:
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
|
if self.tile:
|
||||||
return ImageFile.ImageFile.load(self)
|
args = self.tile[0].args
|
||||||
|
assert isinstance(args, tuple)
|
||||||
|
compression, band = args
|
||||||
|
|
||||||
offset, compression = self.tile[0][2:]
|
self.fp.seek(self.tile[0].offset)
|
||||||
|
|
||||||
self.fp.seek(offset)
|
|
||||||
|
|
||||||
# Copy image data to temporary file
|
# Copy image data to temporary file
|
||||||
o = BytesIO()
|
o = BytesIO()
|
||||||
|
@ -153,10 +151,15 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
size -= len(s)
|
size -= len(s)
|
||||||
|
|
||||||
with Image.open(o) as _im:
|
with Image.open(o) as _im:
|
||||||
|
if band is not None:
|
||||||
|
bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
|
||||||
|
bands[band] = _im
|
||||||
|
_im = Image.merge(self.mode, bands)
|
||||||
|
else:
|
||||||
_im.load()
|
_im.load()
|
||||||
self.im = _im.im
|
self.im = _im.im
|
||||||
self.tile = []
|
self.tile = []
|
||||||
return Image.Image.load(self)
|
return ImageFile.ImageFile.load(self)
|
||||||
|
|
||||||
|
|
||||||
Image.register_open(IptcImageFile.format, IptcImageFile)
|
Image.register_open(IptcImageFile.format, IptcImageFile)
|
||||||
|
|
|
@ -18,11 +18,15 @@ from __future__ import annotations
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
from collections.abc import Callable
|
from typing import cast
|
||||||
from typing import IO, cast
|
|
||||||
|
|
||||||
from . import Image, ImageFile, ImagePalette, _binary
|
from . import Image, ImageFile, ImagePalette, _binary
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
|
||||||
class BoxReader:
|
class BoxReader:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -42,7 +42,6 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from typing import IO, Any
|
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
@ -53,6 +52,8 @@ from .JpegPresets import presets
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from typing import IO, Any
|
||||||
|
|
||||||
from .MpoImagePlugin import MpoImageFile
|
from .MpoImagePlugin import MpoImageFile
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -845,16 +846,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
|
||||||
# ALTERNATIVE: handle JPEGs via the IJG command line utilities.
|
|
||||||
tempfile = im._dump()
|
|
||||||
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])
|
|
||||||
try:
|
|
||||||
os.unlink(tempfile)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Factory for making JPEG and MPO instances
|
# Factory for making JPEG and MPO instances
|
||||||
def jpeg_factory(
|
def jpeg_factory(
|
||||||
|
|
|
@ -32,7 +32,7 @@ class PcdImageFile(ImageFile.ImageFile):
|
||||||
assert self.fp is not None
|
assert self.fp is not None
|
||||||
|
|
||||||
self.fp.seek(2048)
|
self.fp.seek(2048)
|
||||||
s = self.fp.read(2048)
|
s = self.fp.read(1539)
|
||||||
|
|
||||||
if not s.startswith(b"PCD_"):
|
if not s.startswith(b"PCD_"):
|
||||||
msg = "not a PCD file"
|
msg = "not a PCD file"
|
||||||
|
@ -46,14 +46,13 @@ class PcdImageFile(ImageFile.ImageFile):
|
||||||
self.tile_post_rotate = -90
|
self.tile_post_rotate = -90
|
||||||
|
|
||||||
self._mode = "RGB"
|
self._mode = "RGB"
|
||||||
self._size = 768, 512 # FIXME: not correct for rotated images!
|
self._size = (512, 768) if orientation in (1, 3) else (768, 512)
|
||||||
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
|
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
|
||||||
|
|
||||||
def load_end(self) -> None:
|
def load_end(self) -> None:
|
||||||
if self.tile_post_rotate:
|
if self.tile_post_rotate:
|
||||||
# Handle rotated PCDs
|
# Handle rotated PCDs
|
||||||
self.im = self.im.rotate(self.tile_post_rotate)
|
self.im = self.im.rotate(self.tile_post_rotate)
|
||||||
self._size = self.im.size
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
from typing import BinaryIO, Callable
|
|
||||||
|
|
||||||
from . import FontFile, Image
|
from . import FontFile, Image
|
||||||
from ._binary import i8
|
from ._binary import i8
|
||||||
|
@ -27,6 +26,11 @@ from ._binary import i16le as l16
|
||||||
from ._binary import i32be as b32
|
from ._binary import i32be as b32
|
||||||
from ._binary import i32le as l32
|
from ._binary import i32le as l32
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# declarations
|
# declarations
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix: bytes) -> bool:
|
def _accept(prefix: bytes) -> bool:
|
||||||
return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
|
return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -8,7 +8,15 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import zlib
|
import zlib
|
||||||
from typing import IO, Any, NamedTuple, Union
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
_DictBase = collections.UserDict[str | bytes, Any]
|
||||||
|
else:
|
||||||
|
_DictBase = collections.UserDict
|
||||||
|
|
||||||
|
|
||||||
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
|
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
|
||||||
|
@ -251,13 +259,6 @@ class PdfArray(list[Any]):
|
||||||
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
|
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
|
||||||
|
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
_DictBase = collections.UserDict[Union[str, bytes], Any]
|
|
||||||
else:
|
|
||||||
_DictBase = collections.UserDict
|
|
||||||
|
|
||||||
|
|
||||||
class PdfDict(_DictBase):
|
class PdfDict(_DictBase):
|
||||||
def __setattr__(self, key: str, value: Any) -> None:
|
def __setattr__(self, key: str, value: Any) -> None:
|
||||||
if key == "data":
|
if key == "data":
|
||||||
|
|
|
@ -38,9 +38,8 @@ import re
|
||||||
import struct
|
import struct
|
||||||
import warnings
|
import warnings
|
||||||
import zlib
|
import zlib
|
||||||
from collections.abc import Callable
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import IO, Any, NamedTuple, NoReturn, cast
|
from typing import IO, NamedTuple, cast
|
||||||
|
|
||||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
@ -53,6 +52,9 @@ from ._util import DeferredError
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, NoReturn
|
||||||
|
|
||||||
from . import _imaging
|
from . import _imaging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
@ -47,7 +47,7 @@ MODES = {
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix: bytes) -> bool:
|
def _accept(prefix: bytes) -> bool:
|
||||||
return prefix.startswith(b"P") and prefix[1] in b"0123456fy"
|
return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy"
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -47,22 +47,24 @@ import math
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Iterator, MutableMapping
|
from collections.abc import Callable, MutableMapping
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from numbers import Number, Rational
|
from numbers import Number, Rational
|
||||||
from typing import IO, Any, Callable, NoReturn, cast
|
from typing import IO, Any, cast
|
||||||
|
|
||||||
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
from ._binary import i32be as i32
|
from ._binary import i32be as i32
|
||||||
from ._binary import o8
|
from ._binary import o8
|
||||||
from ._typing import StrOrBytesPath
|
|
||||||
from ._util import DeferredError, is_path
|
from ._util import DeferredError, is_path
|
||||||
from .TiffTags import TYPES
|
from .TiffTags import TYPES
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._typing import Buffer, IntegralLike
|
from collections.abc import Iterator
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
|
from ._typing import Buffer, IntegralLike, StrOrBytesPath
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,7 @@ class WalImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
# strings are null-terminated
|
# strings are null-terminated
|
||||||
self.info["name"] = header[:32].split(b"\0", 1)[0]
|
self.info["name"] = header[:32].split(b"\0", 1)[0]
|
||||||
next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
|
if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]:
|
||||||
if next_name:
|
|
||||||
self.info["next_name"] = next_name
|
self.info["next_name"] = next_name
|
||||||
|
|
||||||
def load(self) -> Image.core.PixelAccess | None:
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user