mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-05-31 19:23:16 +03:00
Merge branch 'main' into improved_dds
This commit is contained in:
commit
49578f0059
|
@ -10,7 +10,7 @@ environment:
|
||||||
TEST_OPTIONS:
|
TEST_OPTIONS:
|
||||||
DEPLOY: YES
|
DEPLOY: YES
|
||||||
matrix:
|
matrix:
|
||||||
- PYTHON: C:/Python311
|
- PYTHON: C:/Python312
|
||||||
ARCHITECTURE: x86
|
ARCHITECTURE: x86
|
||||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||||
- PYTHON: C:/Python38-x64
|
- PYTHON: C:/Python38-x64
|
||||||
|
@ -43,7 +43,7 @@ build_script:
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- cd c:\pillow
|
- cd c:\pillow
|
||||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
|
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
|
||||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
||||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
||||||
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
||||||
|
|
|
@ -28,7 +28,8 @@ fi
|
||||||
|
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
python3 -m pip install --upgrade wheel
|
python3 -m pip install --upgrade wheel
|
||||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
# TODO Update condition when cffi supports 3.13
|
||||||
|
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
python3 -m pip install defusedxml
|
python3 -m pip install defusedxml
|
||||||
python3 -m pip install olefile
|
python3 -m pip install olefile
|
||||||
|
@ -38,7 +39,8 @@ python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
python3 -m pip install numpy
|
# TODO Update condition when NumPy supports 3.13
|
||||||
|
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
|
||||||
|
|
||||||
# PyQt6 doesn't support PyPy3
|
# PyQt6 doesn't support PyPy3
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
|
@ -46,6 +48,16 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
python3 -m pip install pyqt6
|
python3 -m pip install pyqt6
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Pyroma uses non-isolated build and fails with old setuptools
|
||||||
|
if [[
|
||||||
|
$GHA_PYTHON_VERSION == pypy3.9
|
||||||
|
|| $GHA_PYTHON_VERSION == 3.8
|
||||||
|
|| $GHA_PYTHON_VERSION == 3.9
|
||||||
|
]]; then
|
||||||
|
# To match pyproject.toml
|
||||||
|
python3 -m pip install "setuptools>=67.8"
|
||||||
|
fi
|
||||||
|
|
||||||
# webp
|
# webp
|
||||||
pushd depends && ./install_webp.sh && popd
|
pushd depends && ./install_webp.sh && popd
|
||||||
|
|
||||||
|
|
1
.ci/requirements-cibw.txt
Normal file
1
.ci/requirements-cibw.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
cibuildwheel==2.16.2
|
|
@ -13,7 +13,7 @@ indent_style = space
|
||||||
|
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.yml]
|
[*.{toml,yml}]
|
||||||
# Two-space indentation
|
# Two-space indentation
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
|
7
.github/workflows/macos-install.sh
vendored
7
.github/workflows/macos-install.sh
vendored
|
@ -5,7 +5,9 @@ set -e
|
||||||
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
|
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
|
||||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
|
||||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
# TODO Update condition when cffi supports 3.13
|
||||||
|
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
|
||||||
|
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
python3 -m pip install defusedxml
|
python3 -m pip install defusedxml
|
||||||
python3 -m pip install olefile
|
python3 -m pip install olefile
|
||||||
|
@ -14,7 +16,8 @@ python3 -m pip install -U pytest-cov
|
||||||
python3 -m pip install -U pytest-timeout
|
python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
|
||||||
python3 -m pip install numpy
|
# TODO Update condition when NumPy supports 3.13
|
||||||
|
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
2
.github/workflows/test-docker.yml
vendored
2
.github/workflows/test-docker.yml
vendored
|
@ -51,8 +51,8 @@ jobs:
|
||||||
debian-11-bullseye-amd64,
|
debian-11-bullseye-amd64,
|
||||||
debian-12-bookworm-x86,
|
debian-12-bookworm-x86,
|
||||||
debian-12-bookworm-amd64,
|
debian-12-bookworm-amd64,
|
||||||
fedora-37-amd64,
|
|
||||||
fedora-38-amd64,
|
fedora-38-amd64,
|
||||||
|
fedora-39-amd64,
|
||||||
gentoo,
|
gentoo,
|
||||||
ubuntu-20.04-focal-amd64,
|
ubuntu-20.04-focal-amd64,
|
||||||
ubuntu-22.04-jammy-amd64,
|
ubuntu-22.04-jammy-amd64,
|
||||||
|
|
55
.github/workflows/test-windows.yml
vendored
55
.github/workflows/test-windows.yml
vendored
|
@ -32,7 +32,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"]
|
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
@ -59,22 +59,23 @@ jobs:
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
allow-prereleases: true
|
||||||
cache: pip
|
cache: pip
|
||||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||||
|
|
||||||
- name: Print build system information
|
- name: Print build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
|
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
||||||
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
|
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: install
|
id: install
|
||||||
run: |
|
run: |
|
||||||
7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
choco install nasm --no-progress
|
||||||
echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH
|
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
choco install ghostscript --version=10.0.0.20230317
|
choco install ghostscript --version=10.0.0.20230317 --no-progress
|
||||||
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
# Install extra test images
|
# Install extra test images
|
||||||
|
@ -166,7 +167,6 @@ jobs:
|
||||||
- name: Build Pillow
|
- name: Build Pillow
|
||||||
run: |
|
run: |
|
||||||
$FLAGS="-C raqm=vendor -C fribidi=vendor"
|
$FLAGS="-C raqm=vendor -C fribidi=vendor"
|
||||||
if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" }
|
|
||||||
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
|
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
|
||||||
& $env:pythonLocation\python.exe selftest.py --installed
|
& $env:pythonLocation\python.exe selftest.py --installed
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
@ -208,47 +208,6 @@ jobs:
|
||||||
flags: GHA_Windows
|
flags: GHA_Windows
|
||||||
name: ${{ runner.os }} Python ${{ matrix.python-version }}
|
name: ${{ runner.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Build wheel
|
|
||||||
id: wheel
|
|
||||||
if: "github.event_name != 'pull_request'"
|
|
||||||
run: |
|
|
||||||
mkdir fribidi
|
|
||||||
copy winbuild\build\bin\fribidi* fribidi
|
|
||||||
setlocal EnableDelayedExpansion
|
|
||||||
for %%f in (winbuild\build\license\*) do (
|
|
||||||
set x=%%~nf
|
|
||||||
rem Skip FriBiDi license, it is not included in the wheel.
|
|
||||||
set fribidi=!x:~0,7!
|
|
||||||
if NOT !fribidi!==fribidi (
|
|
||||||
rem Skip imagequant license, it is not included in the wheel.
|
|
||||||
set libimagequant=!x:~0,13!
|
|
||||||
if NOT !libimagequant!==libimagequant (
|
|
||||||
echo. >> LICENSE
|
|
||||||
echo ===== %%~nf ===== >> LICENSE
|
|
||||||
echo. >> LICENSE
|
|
||||||
type %%f >> LICENSE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
|
|
||||||
call winbuild\\build\\build_env.cmd
|
|
||||||
%pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable .
|
|
||||||
shell: cmd
|
|
||||||
|
|
||||||
- name: Upload wheel
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
if: "github.event_name != 'pull_request'"
|
|
||||||
with:
|
|
||||||
name: ${{ steps.wheel.outputs.dist }}
|
|
||||||
path: "*.whl"
|
|
||||||
|
|
||||||
- name: Upload fribidi.dll
|
|
||||||
if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: fribidi
|
|
||||||
path: fribidi\*
|
|
||||||
|
|
||||||
success:
|
success:
|
||||||
permissions:
|
permissions:
|
||||||
contents: none
|
contents: none
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -41,6 +41,7 @@ jobs:
|
||||||
python-version: [
|
python-version: [
|
||||||
"pypy3.10",
|
"pypy3.10",
|
||||||
"pypy3.9",
|
"pypy3.9",
|
||||||
|
"3.13",
|
||||||
"3.12",
|
"3.12",
|
||||||
"3.11",
|
"3.11",
|
||||||
"3.10",
|
"3.10",
|
||||||
|
@ -64,6 +65,7 @@ jobs:
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
allow-prereleases: true
|
||||||
cache: pip
|
cache: pip
|
||||||
cache-dependency-path: ".ci/*.sh"
|
cache-dependency-path: ".ci/*.sh"
|
||||||
|
|
||||||
|
|
40
.github/workflows/wheels-build.sh
vendored
40
.github/workflows/wheels-build.sh
vendored
|
@ -1,40 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
|
||||||
# webp, zstd, xz, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
|
|
||||||
# libxdmcp causes an issue on macOS < 11
|
|
||||||
# curl from brew requires zstd, use system curl
|
|
||||||
# if php is installed, brew tries to reinstall these after installing openblas
|
|
||||||
# remove lcms2 and libpng to fix building openjpeg on arm64
|
|
||||||
brew remove --ignore-dependencies webp zstd xz libpng libtiff libxcb libxdmcp curl php lcms2 ghostscript
|
|
||||||
|
|
||||||
brew install pkg-config
|
|
||||||
|
|
||||||
if [[ "$PLAT" == "arm64" ]]; then
|
|
||||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
|
||||||
else
|
|
||||||
export MACOSX_DEPLOYMENT_TARGET="10.10"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$MB_PYTHON_VERSION" == pypy3* ]]; then
|
|
||||||
MB_PYTHON_OSX_VER="10.9"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "::group::Install a virtualenv"
|
|
||||||
source wheels/multibuild/common_utils.sh
|
|
||||||
source wheels/multibuild/travis_steps.sh
|
|
||||||
python3 -m pip install virtualenv
|
|
||||||
before_install
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
echo "::group::Build wheel"
|
|
||||||
build_wheel
|
|
||||||
ls -l "${GITHUB_WORKSPACE}/${WHEEL_SDIR}/"
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
if [[ $MACOSX_DEPLOYMENT_TARGET != "11.0" ]]; then
|
|
||||||
echo "::group::Test wheel"
|
|
||||||
install_run
|
|
||||||
echo "::endgroup::"
|
|
||||||
fi
|
|
151
.github/workflows/wheels-dependencies.sh
vendored
Executable file
151
.github/workflows/wheels-dependencies.sh
vendored
Executable file
|
@ -0,0 +1,151 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Define custom utilities
|
||||||
|
# Test for macOS with [ -n "$IS_MACOS" ]
|
||||||
|
if [ -z "$IS_MACOS" ]; then
|
||||||
|
export MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
|
||||||
|
export MB_ML_VER=${AUDITWHEEL_POLICY:9}
|
||||||
|
fi
|
||||||
|
export PLAT=$CIBW_ARCHS
|
||||||
|
source wheels/multibuild/common_utils.sh
|
||||||
|
source wheels/multibuild/library_builders.sh
|
||||||
|
if [ -z "$IS_MACOS" ]; then
|
||||||
|
source wheels/multibuild/manylinux_utils.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCHIVE_SDIR=pillow-depends-main
|
||||||
|
|
||||||
|
# Package versions for fresh source builds
|
||||||
|
FREETYPE_VERSION=2.13.2
|
||||||
|
HARFBUZZ_VERSION=8.3.0
|
||||||
|
LIBPNG_VERSION=1.6.40
|
||||||
|
JPEGTURBO_VERSION=3.0.1
|
||||||
|
OPENJPEG_VERSION=2.5.0
|
||||||
|
XZ_VERSION=5.4.5
|
||||||
|
TIFF_VERSION=4.6.0
|
||||||
|
LCMS2_VERSION=2.15
|
||||||
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
|
GIFLIB_VERSION=5.1.4
|
||||||
|
else
|
||||||
|
GIFLIB_VERSION=5.2.1
|
||||||
|
fi
|
||||||
|
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
||||||
|
ZLIB_VERSION=1.3
|
||||||
|
else
|
||||||
|
ZLIB_VERSION=1.2.8
|
||||||
|
fi
|
||||||
|
LIBWEBP_VERSION=1.3.2
|
||||||
|
BZIP2_VERSION=1.0.8
|
||||||
|
LIBXCB_VERSION=1.16
|
||||||
|
BROTLI_VERSION=1.1.0
|
||||||
|
|
||||||
|
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||||
|
function build_openjpeg {
|
||||||
|
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
|
||||||
|
(cd $out_dir \
|
||||||
|
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||||
|
&& make install)
|
||||||
|
touch openjpeg-stamp
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
function build_brotli {
|
||||||
|
local cmake=$(get_modern_cmake)
|
||||||
|
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz)
|
||||||
|
(cd $out_dir \
|
||||||
|
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||||
|
&& make install)
|
||||||
|
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
|
||||||
|
cp /usr/local/lib64/libbrotli* /usr/local/lib
|
||||||
|
cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function build {
|
||||||
|
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||||
|
export BUILD_PREFIX="/usr/local"
|
||||||
|
fi
|
||||||
|
build_xz
|
||||||
|
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
||||||
|
yum remove -y zlib-devel
|
||||||
|
fi
|
||||||
|
build_new_zlib
|
||||||
|
|
||||||
|
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||||
|
if [ -n "$IS_MACOS" ]; then
|
||||||
|
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||||
|
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
||||||
|
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||||
|
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||||
|
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
|
||||||
|
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||||
|
fi
|
||||||
|
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
|
||||||
|
|
||||||
|
build_libjpeg_turbo
|
||||||
|
build_tiff
|
||||||
|
build_libpng
|
||||||
|
build_lcms2
|
||||||
|
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||||
|
for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
|
||||||
|
cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
build_openjpeg
|
||||||
|
|
||||||
|
ORIGINAL_CFLAGS=$CFLAGS
|
||||||
|
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
||||||
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
|
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
|
||||||
|
fi
|
||||||
|
build_libwebp
|
||||||
|
CFLAGS=$ORIGINAL_CFLAGS
|
||||||
|
|
||||||
|
build_brotli
|
||||||
|
|
||||||
|
if [ -n "$IS_MACOS" ]; then
|
||||||
|
# Custom freetype build
|
||||||
|
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
|
||||||
|
else
|
||||||
|
build_freetype
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$IS_MACOS" ]; then
|
||||||
|
export FREETYPE_LIBS=-lfreetype
|
||||||
|
export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
|
||||||
|
fi
|
||||||
|
build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
|
||||||
|
if [ -z "$IS_MACOS" ]; then
|
||||||
|
export FREETYPE_LIBS=""
|
||||||
|
export FREETYPE_CFLAGS=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Any stuff that you need to do before you start building the wheels
|
||||||
|
# Runs in the root directory of this repository.
|
||||||
|
curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||||
|
untar pillow-depends-main.zip
|
||||||
|
|
||||||
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
|
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
|
||||||
|
# libxdmcp causes an issue on macOS < 11
|
||||||
|
# if php is installed, brew tries to reinstall these after installing openblas
|
||||||
|
# remove cairo to fix building harfbuzz on arm64
|
||||||
|
# remove lcms2 and libpng to fix building openjpeg on arm64
|
||||||
|
# remove zstd to avoid inclusion on x86_64
|
||||||
|
# curl from brew requires zstd, use system curl
|
||||||
|
brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd
|
||||||
|
|
||||||
|
brew install pkg-config
|
||||||
|
fi
|
||||||
|
|
||||||
|
wrap_wheel_builder build
|
||||||
|
|
||||||
|
# Append licenses
|
||||||
|
for filename in wheels/dependency_licenses/*; do
|
||||||
|
echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
|
||||||
|
cat $filename >> LICENSE
|
||||||
|
done
|
69
.github/workflows/wheels-linux.yml
vendored
69
.github/workflows/wheels-linux.yml
vendored
|
@ -1,69 +0,0 @@
|
||||||
name: Build Linux wheels
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
artifacts-name:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
env:
|
|
||||||
CONFIG_PATH: "wheels/config.sh"
|
|
||||||
REPO_DIR: "."
|
|
||||||
TEST_DEPENDS: "pytest pytest-timeout"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: ${{ matrix.python }} ${{ matrix.mb-ml-libc }}${{ matrix.mb-ml-ver }}
|
|
||||||
runs-on: "ubuntu-latest"
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python: [
|
|
||||||
"pypy3.9-7.3.13",
|
|
||||||
"pypy3.10-7.3.13",
|
|
||||||
"3.8",
|
|
||||||
"3.9",
|
|
||||||
"3.10",
|
|
||||||
"3.11",
|
|
||||||
"3.12",
|
|
||||||
]
|
|
||||||
mb-ml-libc: [ "manylinux" ]
|
|
||||||
mb-ml-ver: [ 2014, "_2_28" ]
|
|
||||||
include:
|
|
||||||
- python: "3.8"
|
|
||||||
mb-ml-libc: "musllinux"
|
|
||||||
mb-ml-ver: "_1_1"
|
|
||||||
- python: "3.9"
|
|
||||||
mb-ml-libc: "musllinux"
|
|
||||||
mb-ml-ver: "_1_1"
|
|
||||||
- python: "3.10"
|
|
||||||
mb-ml-libc: "musllinux"
|
|
||||||
mb-ml-ver: "_1_1"
|
|
||||||
- python: "3.11"
|
|
||||||
mb-ml-libc: "musllinux"
|
|
||||||
mb-ml-ver: "_1_1"
|
|
||||||
- python: "3.12"
|
|
||||||
mb-ml-libc: "musllinux"
|
|
||||||
mb-ml-ver: "_1_1"
|
|
||||||
env:
|
|
||||||
MB_PYTHON_VERSION: ${{ matrix.python }}
|
|
||||||
MB_ML_LIBC: ${{ matrix.mb-ml-libc }}
|
|
||||||
MB_ML_VER: ${{ matrix.mb-ml-ver }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
- name: Build Wheel
|
|
||||||
run: .github/workflows/wheels-build.sh
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: ${{ inputs.artifacts-name }}
|
|
||||||
path: wheelhouse/*.whl
|
|
||||||
# Uncomment to get SSH access for testing
|
|
||||||
# - name: Setup tmate session
|
|
||||||
# if: failure()
|
|
||||||
# uses: mxschmitt/action-tmate@v3
|
|
57
.github/workflows/wheels-macos.yml
vendored
57
.github/workflows/wheels-macos.yml
vendored
|
@ -1,57 +0,0 @@
|
||||||
name: Build macOS wheels
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
artifacts-name:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
env:
|
|
||||||
CONFIG_PATH: "wheels/config.sh"
|
|
||||||
REPO_DIR: "."
|
|
||||||
TEST_DEPENDS: "pytest pytest-timeout"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: ${{ matrix.python }} ${{ matrix.platform }}
|
|
||||||
runs-on: "macos-latest"
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python: [
|
|
||||||
"pypy3.9-7.3.13",
|
|
||||||
"pypy3.10-7.3.13",
|
|
||||||
"3.8",
|
|
||||||
"3.9",
|
|
||||||
"3.10",
|
|
||||||
"3.11",
|
|
||||||
"3.12",
|
|
||||||
]
|
|
||||||
platform: [ "x86_64", "arm64" ]
|
|
||||||
exclude:
|
|
||||||
- python: "pypy3.9-7.3.13"
|
|
||||||
platform: "arm64"
|
|
||||||
- python: "pypy3.10-7.3.13"
|
|
||||||
platform: "arm64"
|
|
||||||
env:
|
|
||||||
PLAT: ${{ matrix.platform }}
|
|
||||||
MB_PYTHON_VERSION: ${{ matrix.python }}
|
|
||||||
TRAVIS_OS_NAME: "osx"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
- name: Build Wheel
|
|
||||||
run: .github/workflows/wheels-build.sh
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: ${{ inputs.artifacts-name }}
|
|
||||||
path: wheelhouse/*.whl
|
|
||||||
# Uncomment to get SSH access for testing
|
|
||||||
# - name: Setup tmate session
|
|
||||||
# if: failure()
|
|
||||||
# uses: mxschmitt/action-tmate@v3
|
|
22
.github/workflows/wheels-test.ps1
vendored
Normal file
22
.github/workflows/wheels-test.ps1
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
param ([string]$venv, [string]$pillow="C:\pillow")
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
Set-PSDebug -Trace 1
|
||||||
|
if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
|
||||||
|
# unlike CPython, PyPy requires Visual C++ Redistributable to be installed
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe'
|
||||||
|
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
|
||||||
|
}
|
||||||
|
$env:path += ";$pillow\winbuild\build\bin\"
|
||||||
|
& "$venv\Scripts\activate.ps1"
|
||||||
|
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
|
||||||
|
cd $pillow
|
||||||
|
& python -VV
|
||||||
|
if (!$?) { exit $LASTEXITCODE }
|
||||||
|
& python selftest.py
|
||||||
|
if (!$?) { exit $LASTEXITCODE }
|
||||||
|
& python -m pytest -vx Tests\check_wheel.py
|
||||||
|
if (!$?) { exit $LASTEXITCODE }
|
||||||
|
& python -m pytest -vx Tests
|
||||||
|
if (!$?) { exit $LASTEXITCODE }
|
25
.github/workflows/wheels-test.sh
vendored
Executable file
25
.github/workflows/wheels-test.sh
vendored
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
brew install fribidi
|
||||||
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
||||||
|
apk add curl fribidi
|
||||||
|
else
|
||||||
|
yum install -y fribidi
|
||||||
|
fi
|
||||||
|
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
|
||||||
|
python3 -m pip install numpy
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "test-images-main" ]; then
|
||||||
|
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||||
|
unzip pillow-test-images.zip
|
||||||
|
mv test-images-main/* Tests/images
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Runs tests
|
||||||
|
python3 selftest.py
|
||||||
|
python3 -m pytest Tests/check_wheel.py
|
||||||
|
python3 -m pytest
|
188
.github/workflows/wheels.yml
vendored
188
.github/workflows/wheels.yml
vendored
|
@ -3,14 +3,20 @@ name: Wheels
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/wheels*.yml"
|
- ".ci/requirements-cibw.txt"
|
||||||
|
- ".github/workflows/wheel*"
|
||||||
- "wheels/*"
|
- "wheels/*"
|
||||||
|
- "winbuild/build_prepare.py"
|
||||||
|
- "winbuild/fribidi.cmake"
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/wheels*.yml"
|
- ".ci/requirements-cibw.txt"
|
||||||
|
- ".github/workflows/wheel*"
|
||||||
- "wheels/*"
|
- "wheels/*"
|
||||||
|
- "winbuild/build_prepare.py"
|
||||||
|
- "winbuild/fribidi.cmake"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -20,21 +26,179 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
env:
|
||||||
macos:
|
FORCE_COLOR: 1
|
||||||
uses: ./.github/workflows/wheels-macos.yml
|
|
||||||
with:
|
|
||||||
artifacts-name: "wheels"
|
|
||||||
|
|
||||||
linux:
|
jobs:
|
||||||
uses: ./.github/workflows/wheels-linux.yml
|
build:
|
||||||
with:
|
name: ${{ matrix.name }}
|
||||||
artifacts-name: "wheels"
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: "macOS x86_64"
|
||||||
|
os: macos-latest
|
||||||
|
archs: x86_64
|
||||||
|
macosx_deployment_target: "10.10"
|
||||||
|
- name: "macOS arm64"
|
||||||
|
os: macos-latest
|
||||||
|
archs: arm64
|
||||||
|
macosx_deployment_target: "11.0"
|
||||||
|
- name: "manylinux2014 and musllinux x86_64"
|
||||||
|
os: ubuntu-latest
|
||||||
|
archs: x86_64
|
||||||
|
- name: "manylinux_2_28 x86_64"
|
||||||
|
os: ubuntu-latest
|
||||||
|
archs: x86_64
|
||||||
|
build: "*manylinux*"
|
||||||
|
manylinux: "manylinux_2_28"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Build wheels
|
||||||
|
run: |
|
||||||
|
python3 -m pip install -r .ci/requirements-cibw.txt
|
||||||
|
python3 -m cibuildwheel --output-dir wheelhouse
|
||||||
|
env:
|
||||||
|
CIBW_ARCHS: ${{ matrix.archs }}
|
||||||
|
CIBW_BUILD: ${{ matrix.build }}
|
||||||
|
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
|
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
|
CIBW_SKIP: pp38-*
|
||||||
|
CIBW_TEST_SKIP: "*-macosx_arm64"
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
|
windows:
|
||||||
|
name: Windows ${{ matrix.arch }}
|
||||||
|
runs-on: windows-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: x86
|
||||||
|
cibw_arch: x86
|
||||||
|
- arch: x64
|
||||||
|
cibw_arch: AMD64
|
||||||
|
- arch: ARM64
|
||||||
|
cibw_arch: ARM64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Checkout extra test images
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: python-pillow/test-images
|
||||||
|
path: Tests\test-images
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Prepare for build
|
||||||
|
run: |
|
||||||
|
choco install nasm --no-progress
|
||||||
|
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
|
# Install extra test images
|
||||||
|
xcopy /S /Y Tests\test-images\* Tests\images
|
||||||
|
|
||||||
|
& python.exe -m pip install -r .ci/requirements-cibw.txt
|
||||||
|
|
||||||
|
# Cannot cross-compile FriBiDi (only used for tests)
|
||||||
|
$FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
|
||||||
|
if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
|
||||||
|
& python.exe winbuild\build_prepare.py -v @FLAGS
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Build wheels
|
||||||
|
run: |
|
||||||
|
setlocal EnableDelayedExpansion
|
||||||
|
for %%f in (winbuild\build\license\*) do (
|
||||||
|
set x=%%~nf
|
||||||
|
rem Skip FriBiDi license, it is not included in the wheel.
|
||||||
|
set fribidi=!x:~0,7!
|
||||||
|
if NOT !fribidi!==fribidi (
|
||||||
|
rem Skip imagequant license, it is not included in the wheel.
|
||||||
|
set libimagequant=!x:~0,13!
|
||||||
|
if NOT !libimagequant!==libimagequant (
|
||||||
|
echo. >> LICENSE
|
||||||
|
echo ===== %%~nf ===== >> LICENSE
|
||||||
|
echo. >> LICENSE
|
||||||
|
type %%f >> LICENSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
call winbuild\\build\\build_env.cmd
|
||||||
|
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
|
||||||
|
env:
|
||||||
|
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||||
|
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||||
|
CIBW_CACHE_PATH: "C:\\cibw"
|
||||||
|
CIBW_TEST_SKIP: "*-win_arm64"
|
||||||
|
CIBW_TEST_COMMAND: 'docker run --rm
|
||||||
|
-v {project}:C:\pillow
|
||||||
|
-v C:\cibw:C:\cibw
|
||||||
|
-v %CD%\..\venv-test:%CD%\..\venv-test
|
||||||
|
-e CI -e GITHUB_ACTIONS
|
||||||
|
mcr.microsoft.com/windows/servercore:ltsc2022
|
||||||
|
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
|
||||||
|
shell: cmd
|
||||||
|
|
||||||
|
- name: Upload wheels
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
|
- name: Prepare to upload FriBiDi
|
||||||
|
if: "matrix.arch != 'ARM64'"
|
||||||
|
run: |
|
||||||
|
mkdir fribidi\${{ matrix.arch }}
|
||||||
|
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
|
||||||
|
shell: cmd
|
||||||
|
|
||||||
|
- name: Upload fribidi.dll
|
||||||
|
if: "matrix.arch != 'ARM64'"
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: fribidi
|
||||||
|
path: fribidi\*
|
||||||
|
|
||||||
|
sdist:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: "Makefile"
|
||||||
|
|
||||||
|
- run: make sdist
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/*.tar.gz
|
||||||
|
|
||||||
success:
|
success:
|
||||||
permissions:
|
permissions:
|
||||||
contents: none
|
contents: none
|
||||||
needs: [macos, linux]
|
needs: [build, windows, sdist]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Wheels Successful
|
name: Wheels Successful
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v3.13.0
|
rev: v0.1.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: ruff
|
||||||
args: [--py38-plus]
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 23.9.1
|
rev: 23.10.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--target-version=py38]
|
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
|
||||||
rev: 5.12.0
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
rev: 1.7.5
|
rev: 1.7.5
|
||||||
|
@ -23,32 +17,19 @@ repos:
|
||||||
args: [--severity-level=high]
|
args: [--severity-level=high]
|
||||||
files: ^src/
|
files: ^src/
|
||||||
|
|
||||||
- repo: https://github.com/asottile/yesqa
|
|
||||||
rev: v1.5.0
|
|
||||||
hooks:
|
|
||||||
- id: yesqa
|
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.5.4
|
rev: v1.5.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 6.1.0
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies:
|
|
||||||
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: v1.10.0
|
rev: v1.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-check-blanket-noqa
|
|
||||||
- id: rst-backticks
|
- id: rst-backticks
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
|
@ -61,17 +42,17 @@ repos:
|
||||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||||
|
|
||||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||||
rev: v0.6.8
|
rev: v0.8.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: 1.2.0
|
rev: 1.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
|
|
||||||
- repo: https://github.com/abravalheri/validate-pyproject
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
rev: v0.14
|
rev: v0.15
|
||||||
hooks:
|
hooks:
|
||||||
- id: validate-pyproject
|
- id: validate-pyproject
|
||||||
|
|
||||||
|
|
119
.travis.yml
119
.travis.yml
|
@ -2,133 +2,50 @@ if: tag IS present OR type = api
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- CONFIG_PATH=wheels/config.sh
|
- CIBW_ARCHS=aarch64
|
||||||
- REPO_DIR=.
|
- CIBW_SKIP=pp38-*
|
||||||
- PLAT=aarch64
|
|
||||||
- TEST_DEPENDS=pytest-timeout
|
|
||||||
|
|
||||||
language: python
|
language: python
|
||||||
# Default Python version is usually 3.6
|
# Default Python version is usually 3.6
|
||||||
python: "3.11"
|
python: "3.12"
|
||||||
dist: focal
|
dist: jammy
|
||||||
services: docker
|
services: docker
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- name: "3.8 Focal manylinux2014 aarch64"
|
- name: "manylinux2014 aarch64"
|
||||||
os: linux
|
os: linux
|
||||||
arch: arm64
|
arch: arm64
|
||||||
env:
|
env:
|
||||||
- MB_ML_VER=2014
|
- CIBW_BUILD="*manylinux*"
|
||||||
- MB_PYTHON_VERSION=3.8
|
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
|
||||||
- name: "3.8 Focal manylinux_2_28 aarch64"
|
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
|
||||||
|
- name: "manylinux_2_28 aarch64"
|
||||||
os: linux
|
os: linux
|
||||||
arch: arm64
|
arch: arm64
|
||||||
env:
|
env:
|
||||||
- MB_ML_VER="_2_28"
|
- CIBW_BUILD="*manylinux*"
|
||||||
- MB_PYTHON_VERSION=3.8
|
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
|
||||||
- name: "3.8 musllinux_1_1 aarch64"
|
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
|
||||||
|
- name: "musllinux aarch64"
|
||||||
os: linux
|
os: linux
|
||||||
arch: arm64
|
arch: arm64
|
||||||
env:
|
env:
|
||||||
- MB_ML_VER="_1_1"
|
- CIBW_BUILD="*musllinux*"
|
||||||
- MB_ML_LIBC="musllinux"
|
|
||||||
- MB_PYTHON_VERSION=3.8
|
|
||||||
- name: "3.9 Focal manylinux2014 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER=2014
|
|
||||||
- MB_PYTHON_VERSION=3.9
|
|
||||||
- name: "3.9 Focal manylinux_2_28 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_2_28"
|
|
||||||
- MB_PYTHON_VERSION=3.9
|
|
||||||
- name: "3.9 musllinux_1_1 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_1_1"
|
|
||||||
- MB_ML_LIBC="musllinux"
|
|
||||||
- MB_PYTHON_VERSION=3.9
|
|
||||||
- name: "3.10 Focal manylinux2014 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER=2014
|
|
||||||
- MB_PYTHON_VERSION=3.10
|
|
||||||
- name: "3.10 Focal manylinux_2_28 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_2_28"
|
|
||||||
- MB_PYTHON_VERSION=3.10
|
|
||||||
- name: "3.10 musllinux_1_1 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_1_1"
|
|
||||||
- MB_ML_LIBC="musllinux"
|
|
||||||
- MB_PYTHON_VERSION=3.10
|
|
||||||
- name: "3.11 Focal manylinux_2_28 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER=2014
|
|
||||||
- MB_PYTHON_VERSION=3.11
|
|
||||||
- name: "3.11 Focal manylinux_2_28 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_2_28"
|
|
||||||
- MB_PYTHON_VERSION=3.11
|
|
||||||
- name: "3.11 musllinux_1_1 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_1_1"
|
|
||||||
- MB_ML_LIBC="musllinux"
|
|
||||||
- MB_PYTHON_VERSION=3.11
|
|
||||||
- name: "3.12 Focal manylinux_2_28 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER=2014
|
|
||||||
- MB_PYTHON_VERSION=3.12
|
|
||||||
- name: "3.12 Focal manylinux_2_28 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_2_28"
|
|
||||||
- MB_PYTHON_VERSION=3.12
|
|
||||||
- name: "3.12 musllinux_1_1 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- MB_ML_VER="_1_1"
|
|
||||||
- MB_ML_LIBC="musllinux"
|
|
||||||
- MB_PYTHON_VERSION=3.12
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- source wheels/multibuild/common_utils.sh
|
|
||||||
- source wheels/multibuild/travis_steps.sh
|
|
||||||
- before_install
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- build_multilinux aarch64 build_wheel
|
- python3 -m pip install -r .ci/requirements-cibw.txt
|
||||||
- ls -l "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/"
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- install_run
|
- python3 -m cibuildwheel --output-dir wheelhouse
|
||||||
|
- ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
|
||||||
|
|
||||||
# Upload wheels to GitHub Releases
|
# Upload wheels to GitHub Releases
|
||||||
deploy:
|
deploy:
|
||||||
provider: releases
|
provider: releases
|
||||||
api_key: $GITHUB_RELEASE_TOKEN
|
api_key: $GITHUB_RELEASE_TOKEN
|
||||||
file_glob: true
|
file_glob: true
|
||||||
file: "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/*.whl"
|
file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
|
||||||
on:
|
on:
|
||||||
repo: python-pillow/Pillow
|
repo: python-pillow/Pillow
|
||||||
tags: true
|
tags: true
|
||||||
|
|
24
CHANGES.rst
24
CHANGES.rst
|
@ -2,6 +2,30 @@
|
||||||
Changelog (Pillow)
|
Changelog (Pillow)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
10.2.0 (unreleased)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Raise ValueError when TrueType font size is not greater than zero #7584
|
||||||
|
[akx, radarhere]
|
||||||
|
|
||||||
|
- If absent, do not try to close fp when closing image #7557
|
||||||
|
[RaphaelVRossi, radarhere]
|
||||||
|
|
||||||
|
- Allow configuring JPEG restart marker interval on save #7488
|
||||||
|
[bgilbert, radarhere]
|
||||||
|
|
||||||
|
- Decrement reference count for PyObject #7549
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491
|
||||||
|
[bgilbert, radarhere]
|
||||||
|
|
||||||
|
- If save_all PNG only has one frame, do not create animated image #7522
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed frombytes() for images with a zero dimension #7493
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
10.1.0 (2023-10-15)
|
10.1.0 (2023-10-15)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,10 @@ include *.md
|
||||||
include *.py
|
include *.py
|
||||||
include *.rst
|
include *.rst
|
||||||
include *.sh
|
include *.sh
|
||||||
|
include *.toml
|
||||||
include *.txt
|
include *.txt
|
||||||
include *.yaml
|
include *.yaml
|
||||||
|
include .flake8
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include Makefile
|
include Makefile
|
||||||
include tox.ini
|
include tox.ini
|
||||||
|
|
8
Makefile
8
Makefile
|
@ -49,7 +49,7 @@ help:
|
||||||
@echo " install make and install"
|
@echo " install make and install"
|
||||||
@echo " install-coverage make and install with C coverage"
|
@echo " install-coverage make and install with C coverage"
|
||||||
@echo " lint run the lint checks"
|
@echo " lint run the lint checks"
|
||||||
@echo " lint-fix run Black and isort to (mostly) fix lint issues"
|
@echo " lint-fix run Ruff to (mostly) fix lint issues"
|
||||||
@echo " release-test run code and package tests before release"
|
@echo " release-test run code and package tests before release"
|
||||||
@echo " test run tests on installed Pillow"
|
@echo " test run tests on installed Pillow"
|
||||||
|
|
||||||
|
@ -118,6 +118,6 @@ lint:
|
||||||
.PHONY: lint-fix
|
.PHONY: lint-fix
|
||||||
lint-fix:
|
lint-fix:
|
||||||
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
|
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
|
||||||
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
|
python3 -m black .
|
||||||
python3 -m black --target-version py38 .
|
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
|
||||||
python3 -m isort .
|
python3 -m ruff --fix .
|
||||||
|
|
20
RELEASING.md
20
RELEASING.md
|
@ -20,12 +20,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
git tag 5.2.0
|
git tag 5.2.0
|
||||||
git push --tags
|
git push --tags
|
||||||
```
|
```
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||||
```bash
|
* [ ] Check and upload all source and binary distributions e.g.:
|
||||||
make sdist
|
|
||||||
```
|
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
|
||||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m twine check --strict dist/*
|
python3 -m twine check --strict dist/*
|
||||||
python3 -m twine upload dist/Pillow-5.2.0*
|
python3 -m twine upload dist/Pillow-5.2.0*
|
||||||
|
@ -59,8 +55,8 @@ Released as needed for security, installation or critical bug fixes.
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
* [ ] Check and upload all source and binary distributions e.g.:
|
||||||
```bash
|
```bash
|
||||||
python3 -m twine check --strict dist/*
|
python3 -m twine check --strict dist/*
|
||||||
python3 -m twine upload dist/Pillow-5.2.1*
|
python3 -m twine upload dist/Pillow-5.2.1*
|
||||||
|
@ -90,20 +86,20 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||||
```bash
|
```bash
|
||||||
git push origin 2.5.x
|
git push origin 2.5.x
|
||||||
```
|
```
|
||||||
|
|
||||||
## Binary Distributions
|
## Source and Binary Distributions
|
||||||
|
|
||||||
### macOS and Linux
|
### macOS and Linux
|
||||||
* [ ] Download wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||||
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
|
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
|
||||||
```bash
|
```bash
|
||||||
gh run download --dir dist
|
gh run download --dir dist
|
||||||
# select wheels
|
# select dist
|
||||||
```
|
```
|
||||||
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
|
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
|
||||||
and copy into `dist`.
|
and copy into `dist`.
|
||||||
|
|
|
@ -45,7 +45,7 @@ def test_direct():
|
||||||
|
|
||||||
assert caccess[(0, 0)] == access[(0, 0)]
|
assert caccess[(0, 0)] == access[(0, 0)]
|
||||||
|
|
||||||
print("Size: %sx%s" % im.size)
|
print(f"Size: {im.width}x{im.height}")
|
||||||
timer(iterate_get, "PyAccess - get", im.size, access)
|
timer(iterate_get, "PyAccess - get", im.size, access)
|
||||||
timer(iterate_set, "PyAccess - set", im.size, access)
|
timer(iterate_set, "PyAccess - set", im.size, access)
|
||||||
timer(iterate_get, "C-api - get", im.size, caccess)
|
timer(iterate_get, "C-api - get", im.size, caccess)
|
||||||
|
|
41
Tests/check_wheel.py
Normal file
41
Tests/check_wheel.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from PIL import features
|
||||||
|
|
||||||
|
|
||||||
|
def test_wheel_modules():
|
||||||
|
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
|
||||||
|
|
||||||
|
# tkinter is not available in cibuildwheel installed CPython on Windows
|
||||||
|
try:
|
||||||
|
import tkinter
|
||||||
|
|
||||||
|
assert tkinter
|
||||||
|
except ImportError:
|
||||||
|
expected_modules.remove("tkinter")
|
||||||
|
|
||||||
|
assert set(features.get_supported_modules()) == expected_modules
|
||||||
|
|
||||||
|
|
||||||
|
def test_wheel_codecs():
|
||||||
|
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
|
||||||
|
|
||||||
|
assert set(features.get_supported_codecs()) == expected_codecs
|
||||||
|
|
||||||
|
|
||||||
|
def test_wheel_features():
|
||||||
|
expected_features = {
|
||||||
|
"webp_anim",
|
||||||
|
"webp_mux",
|
||||||
|
"transp_webp",
|
||||||
|
"raqm",
|
||||||
|
"fribidi",
|
||||||
|
"harfbuzz",
|
||||||
|
"libjpeg_turbo",
|
||||||
|
"xcb",
|
||||||
|
}
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
expected_features.remove("xcb")
|
||||||
|
|
||||||
|
assert set(features.get_supported_features()) == expected_features
|
|
@ -5,6 +5,7 @@ Helper functions.
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -95,7 +96,7 @@ def assert_image_equal(a, b, msg=None):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert False, msg or "got different content"
|
pytest.fail(msg or "got different content")
|
||||||
|
|
||||||
|
|
||||||
def assert_image_equal_tofile(a, filename, msg=None, mode=None):
|
def assert_image_equal_tofile(a, filename, msg=None, mode=None):
|
||||||
|
@ -258,11 +259,21 @@ def hopper(mode=None, cache={}):
|
||||||
|
|
||||||
|
|
||||||
def djpeg_available():
|
def djpeg_available():
|
||||||
return bool(shutil.which("djpeg"))
|
if shutil.which("djpeg"):
|
||||||
|
try:
|
||||||
|
subprocess.check_call(["djpeg", "-version"])
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def cjpeg_available():
|
def cjpeg_available():
|
||||||
return bool(shutil.which("cjpeg"))
|
if shutil.which("cjpeg"):
|
||||||
|
try:
|
||||||
|
subprocess.check_call(["cjpeg", "-version"])
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def netpbm_available():
|
def netpbm_available():
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
python3 setup.py build --build-base=/tmp/build install
|
python3 -m pip install .
|
||||||
|
|
||||||
# Build fuzzers in $OUT.
|
# Build fuzzers in $OUT.
|
||||||
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
|
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
|
||||||
|
|
|
@ -350,7 +350,7 @@ def test_apng_save(tmp_path):
|
||||||
im.load()
|
im.load()
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert im.get_format_mimetype() == "image/apng"
|
assert im.get_format_mimetype() == "image/png"
|
||||||
assert im.info.get("default_image") is None
|
assert im.info.get("default_image") is None
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
@ -450,26 +450,29 @@ def test_apng_save_duration_loop(tmp_path):
|
||||||
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
||||||
)
|
)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
im.load()
|
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert im.info.get("duration") == 750
|
assert "duration" not in im.info
|
||||||
|
|
||||||
|
different_frame = Image.new("RGBA", (128, 64))
|
||||||
|
frame.save(
|
||||||
|
test_file,
|
||||||
|
save_all=True,
|
||||||
|
append_images=[frame, different_frame],
|
||||||
|
duration=[500, 100, 150],
|
||||||
|
)
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
assert im.n_frames == 2
|
||||||
|
assert im.info["duration"] == 600
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
assert im.info["duration"] == 150
|
||||||
|
|
||||||
# test info duration
|
# test info duration
|
||||||
frame.info["duration"] = 750
|
frame.info["duration"] = 300
|
||||||
frame.save(test_file, save_all=True)
|
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.info.get("duration") == 750
|
assert im.n_frames == 2
|
||||||
|
assert im.info["duration"] == 600
|
||||||
|
|
||||||
def test_apng_save_duplicate_duration(tmp_path):
|
|
||||||
test_file = str(tmp_path / "temp.png")
|
|
||||||
frame = Image.new("RGB", (1, 1))
|
|
||||||
|
|
||||||
# Test a single duration is correctly combined across duplicate frames
|
|
||||||
frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
|
|
||||||
with Image.open(test_file) as im:
|
|
||||||
assert im.n_frames == 1
|
|
||||||
assert im.info.get("duration") == 1500
|
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_disposal(tmp_path):
|
def test_apng_save_disposal(tmp_path):
|
||||||
|
@ -674,7 +677,8 @@ def test_seek_after_close():
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
||||||
@pytest.mark.parametrize("default_image", (True, False))
|
@pytest.mark.parametrize("default_image", (True, False))
|
||||||
def test_different_modes_in_later_frames(mode, default_image, tmp_path):
|
@pytest.mark.parametrize("duplicate", (True, False))
|
||||||
|
def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
im = Image.new("L", (1, 1))
|
im = Image.new("L", (1, 1))
|
||||||
|
@ -682,7 +686,7 @@ def test_different_modes_in_later_frames(mode, default_image, tmp_path):
|
||||||
test_file,
|
test_file,
|
||||||
save_all=True,
|
save_all=True,
|
||||||
default_image=default_image,
|
default_image=default_image,
|
||||||
append_images=[Image.new(mode, (1, 1))],
|
append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
|
||||||
)
|
)
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
assert reloaded.mode == mode
|
assert reloaded.mode == mode
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import sys
|
import sys
|
||||||
from io import BytesIO, StringIO
|
from io import BytesIO, StringIO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, IptcImagePlugin
|
from PIL import Image, IptcImagePlugin
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper
|
||||||
|
@ -44,7 +46,7 @@ def test_getiptcinfo_fotostation():
|
||||||
for tag in iptc.keys():
|
for tag in iptc.keys():
|
||||||
if tag[0] == 240:
|
if tag[0] == 240:
|
||||||
return
|
return
|
||||||
assert False, "FotoStation tag not found"
|
pytest.fail("FotoStation tag not found")
|
||||||
|
|
||||||
|
|
||||||
def test_getiptcinfo_zero_padding():
|
def test_getiptcinfo_zero_padding():
|
||||||
|
|
|
@ -643,6 +643,23 @@ class TestFileJpeg:
|
||||||
assert max(im2.quantization[0]) <= 255
|
assert max(im2.quantization[0]) <= 255
|
||||||
assert max(im2.quantization[1]) <= 255
|
assert max(im2.quantization[1]) <= 255
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"blocks, rows, markers",
|
||||||
|
((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
|
||||||
|
)
|
||||||
|
def test_restart_markers(self, blocks, rows, markers):
|
||||||
|
im = Image.new("RGB", (32, 32)) # 16 MCUs
|
||||||
|
out = BytesIO()
|
||||||
|
im.save(
|
||||||
|
out,
|
||||||
|
format="JPEG",
|
||||||
|
restart_marker_blocks=blocks,
|
||||||
|
restart_marker_rows=rows,
|
||||||
|
# force 8x8 pixel MCUs
|
||||||
|
subsampling=0,
|
||||||
|
)
|
||||||
|
assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers
|
||||||
|
|
||||||
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
||||||
def test_load_djpeg(self):
|
def test_load_djpeg(self):
|
||||||
with Image.open(TEST_FILE) as img:
|
with Image.open(TEST_FILE) as img:
|
||||||
|
@ -961,6 +978,28 @@ class TestFileJpeg:
|
||||||
im.load()
|
im.load()
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
|
def test_separate_tables(self):
|
||||||
|
im = hopper()
|
||||||
|
data = [] # [interchange, tables-only, image-only]
|
||||||
|
for streamtype in range(3):
|
||||||
|
out = BytesIO()
|
||||||
|
im.save(out, format="JPEG", streamtype=streamtype)
|
||||||
|
data.append(out.getvalue())
|
||||||
|
|
||||||
|
# SOI, EOI
|
||||||
|
for marker in b"\xff\xd8", b"\xff\xd9":
|
||||||
|
assert marker in data[1] and marker in data[2]
|
||||||
|
# DHT, DQT
|
||||||
|
for marker in b"\xff\xc4", b"\xff\xdb":
|
||||||
|
assert marker in data[1] and marker not in data[2]
|
||||||
|
# SOF0, SOS, APP0 (JFIF header)
|
||||||
|
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
|
||||||
|
assert marker not in data[1] and marker in data[2]
|
||||||
|
|
||||||
|
with Image.open(BytesIO(data[0])) as interchange_im:
|
||||||
|
with Image.open(BytesIO(data[1] + data[2])) as combined_im:
|
||||||
|
assert_image_equal(interchange_im, combined_im)
|
||||||
|
|
||||||
def test_repr_jpeg(self):
|
def test_repr_jpeg(self):
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
||||||
|
|
|
@ -416,7 +416,7 @@ def test_plt_marker():
|
||||||
while True:
|
while True:
|
||||||
marker = out.read(2)
|
marker = out.read(2)
|
||||||
if not marker:
|
if not marker:
|
||||||
assert False, "End of stream without PLT"
|
pytest.fail("End of stream without PLT")
|
||||||
|
|
||||||
jp2_boxid = _binary.i16be(marker)
|
jp2_boxid = _binary.i16be(marker)
|
||||||
if jp2_boxid == 0xFF4F:
|
if jp2_boxid == 0xFF4F:
|
||||||
|
@ -426,7 +426,7 @@ def test_plt_marker():
|
||||||
# PLT
|
# PLT
|
||||||
return
|
return
|
||||||
elif jp2_boxid == 0xFF93:
|
elif jp2_boxid == 0xFF93:
|
||||||
assert False, "SOD without finding PLT first"
|
pytest.fail("SOD without finding PLT first")
|
||||||
|
|
||||||
hdr = out.read(2)
|
hdr = out.read(2)
|
||||||
length = _binary.i16be(hdr)
|
length = _binary.i16be(hdr)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import io
|
import io
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
@ -906,6 +907,13 @@ class TestImage:
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
assert im.tobytes() == b""
|
assert im.tobytes() == b""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||||
|
def test_zero_frombytes(self, size):
|
||||||
|
Image.frombytes("RGB", size, b"")
|
||||||
|
|
||||||
|
im = Image.new("RGB", size)
|
||||||
|
im.frombytes(b"")
|
||||||
|
|
||||||
def test_has_transparency_data(self):
|
def test_has_transparency_data(self):
|
||||||
for mode in ("1", "L", "P", "RGB"):
|
for mode in ("1", "L", "P", "RGB"):
|
||||||
im = Image.new(mode, (1, 1))
|
im = Image.new(mode, (1, 1))
|
||||||
|
@ -992,7 +1000,7 @@ class TestImage:
|
||||||
with Image.open(os.path.join("Tests/images", path)) as im:
|
with Image.open(os.path.join("Tests/images", path)) as im:
|
||||||
try:
|
try:
|
||||||
im.load()
|
im.load()
|
||||||
assert False
|
pytest.fail()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
buffer_overrun = str(e) == "buffer overrun when reading image file"
|
buffer_overrun = str(e) == "buffer overrun when reading image file"
|
||||||
truncated = "image file is truncated" in str(e)
|
truncated = "image file is truncated" in str(e)
|
||||||
|
@ -1003,10 +1011,19 @@ class TestImage:
|
||||||
with Image.open("Tests/images/fli_overrun2.bin") as im:
|
with Image.open("Tests/images/fli_overrun2.bin") as im:
|
||||||
try:
|
try:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert False
|
pytest.fail()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
assert str(e) == "buffer overrun when reading image file"
|
assert str(e) == "buffer overrun when reading image file"
|
||||||
|
|
||||||
|
def test_close_graceful(self, caplog):
|
||||||
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
|
copy = im.copy()
|
||||||
|
with caplog.at_level(logging.DEBUG):
|
||||||
|
im.close()
|
||||||
|
copy.close()
|
||||||
|
assert len(caplog.records) == 0
|
||||||
|
assert im.fp is None
|
||||||
|
|
||||||
|
|
||||||
class MockEncoder:
|
class MockEncoder:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -67,7 +67,7 @@ def test_quantize_no_dither():
|
||||||
|
|
||||||
def test_quantize_no_dither2():
|
def test_quantize_no_dither2():
|
||||||
im = Image.new("RGB", (9, 1))
|
im = Image.new("RGB", (9, 1))
|
||||||
im.putdata(list((p,) * 3 for p in range(0, 36, 4)))
|
im.putdata([(p,) * 3 for p in range(0, 36, 4)])
|
||||||
|
|
||||||
palette = Image.new("P", (1, 1))
|
palette = Image.new("P", (1, 1))
|
||||||
data = (0, 0, 0, 32, 32, 32)
|
data = (0, 0, 0, 32, 32, 32)
|
||||||
|
|
|
@ -195,7 +195,7 @@ class TestReducingGapResize:
|
||||||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
|
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
assert_image_equal(ref, im)
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
assert_image_similar(ref, im, epsilon)
|
assert_image_similar(ref, im, epsilon)
|
||||||
|
@ -210,7 +210,7 @@ class TestReducingGapResize:
|
||||||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
|
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
assert_image_equal(ref, im)
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
assert_image_similar(ref, im, epsilon)
|
assert_image_similar(ref, im, epsilon)
|
||||||
|
@ -225,7 +225,7 @@ class TestReducingGapResize:
|
||||||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
|
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
assert_image_equal(ref, im)
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
assert_image_similar(ref, im, epsilon)
|
assert_image_similar(ref, im, epsilon)
|
||||||
|
|
|
@ -147,7 +147,7 @@ def test_reducing_gap_values():
|
||||||
|
|
||||||
ref = hopper()
|
ref = hopper()
|
||||||
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
|
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(pytest.fail.Exception):
|
||||||
assert_image_equal(ref, im)
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
assert_image_similar(ref, im, 3.5)
|
assert_image_similar(ref, im, 3.5)
|
||||||
|
|
|
@ -1071,3 +1071,9 @@ def test_raqm_missing_warning(monkeypatch):
|
||||||
"Raqm layout was requested, but Raqm is not available. "
|
"Raqm layout was requested, but Raqm is not available. "
|
||||||
"Falling back to basic layout."
|
"Falling back to basic layout."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("size", [-1, 0])
|
||||||
|
def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
|
||||||
|
|
|
@ -11,6 +11,10 @@ from .helper import assert_image_equal_tofile, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
class TestImageGrab:
|
class TestImageGrab:
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
os.environ.get("USERNAME") == "ContainerAdministrator",
|
||||||
|
reason="can't grab screen when running in Docker",
|
||||||
|
)
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
|
sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
|
||||||
)
|
)
|
||||||
|
|
|
@ -433,6 +433,12 @@ def test_exif_transpose_in_place():
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocontrast_unsupported_mode():
|
||||||
|
im = Image.new("RGBA", (1, 1))
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
ImageOps.autocontrast(im)
|
||||||
|
|
||||||
|
|
||||||
def test_autocontrast_cutoff():
|
def test_autocontrast_cutoff():
|
||||||
# Test the cutoff argument of autocontrast
|
# Test the cutoff argument of autocontrast
|
||||||
with Image.open("Tests/images/bw_gradient.png") as img:
|
with Image.open("Tests/images/bw_gradient.png") as img:
|
||||||
|
|
|
@ -85,7 +85,7 @@ def test_ipythonviewer():
|
||||||
test_viewer = viewer
|
test_viewer = viewer
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
assert False
|
pytest.fail()
|
||||||
|
|
||||||
im = hopper()
|
im = hopper()
|
||||||
assert test_viewer.show(im) == 1
|
assert test_viewer.show(im) == 1
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from setuptools.build_meta import * # noqa: F401, F403
|
from setuptools.build_meta import * # noqa: F403
|
||||||
from setuptools.build_meta import build_wheel
|
from setuptools.build_meta import build_wheel
|
||||||
|
|
||||||
backend_class = build_wheel.__self__.__class__
|
backend_class = build_wheel.__self__.__class__
|
||||||
|
|
25
docs/conf.py
25
docs/conf.py
|
@ -166,6 +166,12 @@ html_static_path = ["resources"]
|
||||||
# directly to the root of the documentation.
|
# directly to the root of the documentation.
|
||||||
# html_extra_path = []
|
# html_extra_path = []
|
||||||
|
|
||||||
|
html_css_files = ["css/dark.css"]
|
||||||
|
|
||||||
|
html_js_files = [
|
||||||
|
"js/activate_tab.js",
|
||||||
|
]
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
# using the given strftime format.
|
# using the given strftime format.
|
||||||
# html_last_updated_fmt = '%b %d, %Y'
|
# html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
@ -313,19 +319,15 @@ texinfo_documents = [
|
||||||
# texinfo_no_detailmenu = False
|
# texinfo_no_detailmenu = False
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
|
||||||
app.add_css_file("css/dark.css")
|
|
||||||
|
|
||||||
|
|
||||||
linkcheck_allowed_redirects = {
|
linkcheck_allowed_redirects = {
|
||||||
r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", # noqa: E501
|
r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
|
||||||
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
|
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",
|
||||||
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
|
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
|
||||||
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501
|
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
|
||||||
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
|
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
|
||||||
r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501
|
r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
|
||||||
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501
|
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
|
||||||
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501
|
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
|
||||||
}
|
}
|
||||||
|
|
||||||
# sphinx.ext.extlinks
|
# sphinx.ext.extlinks
|
||||||
|
@ -338,6 +340,7 @@ extlinks = {
|
||||||
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
|
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
|
||||||
"issue": (_repo + "issues/%s", "#%s"),
|
"issue": (_repo + "issues/%s", "#%s"),
|
||||||
"pr": (_repo + "pull/%s", "#%s"),
|
"pr": (_repo + "pull/%s", "#%s"),
|
||||||
|
"pypi": ("https://pypi.org/project/%s/", "%s"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# sphinxext.opengraph
|
# sphinxext.opengraph
|
||||||
|
|
|
@ -10,7 +10,7 @@ Deprecated features
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
Below are features which are considered deprecated. Where appropriate,
|
Below are features which are considered deprecated. Where appropriate,
|
||||||
a ``DeprecationWarning`` is issued.
|
a :py:exc:`DeprecationWarning` is issued.
|
||||||
|
|
||||||
PSFile
|
PSFile
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
@ -267,7 +267,7 @@ ImageFile.raise_ioerror
|
||||||
.. deprecated:: 7.2.0
|
.. deprecated:: 7.2.0
|
||||||
.. versionremoved:: 9.0.0
|
.. versionremoved:: 9.0.0
|
||||||
|
|
||||||
``IOError`` was merged into ``OSError`` in Python 3.3.
|
:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3.
|
||||||
So, ``ImageFile.raise_ioerror`` has been removed.
|
So, ``ImageFile.raise_ioerror`` has been removed.
|
||||||
Use ``ImageFile.raise_oserror`` instead.
|
Use ``ImageFile.raise_oserror`` instead.
|
||||||
|
|
||||||
|
@ -293,9 +293,9 @@ im.offset
|
||||||
``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
|
``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
|
||||||
|
|
||||||
It was documented as deprecated in PIL 1.1.2,
|
It was documented as deprecated in PIL 1.1.2,
|
||||||
raised a ``DeprecationWarning`` since 1.1.5,
|
raised a :py:exc:`DeprecationWarning` since 1.1.5,
|
||||||
an ``Exception`` since Pillow 3.0.0
|
an :py:exc:`Exception` since Pillow 3.0.0
|
||||||
and ``NotImplementedError`` since 3.3.0.
|
and :py:exc:`NotImplementedError` since 3.3.0.
|
||||||
|
|
||||||
Image.fromstring, im.fromstring and im.tostring
|
Image.fromstring, im.fromstring and im.tostring
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -307,9 +307,9 @@ Image.fromstring, im.fromstring and im.tostring
|
||||||
* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
|
* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
|
||||||
* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
|
* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
|
||||||
|
|
||||||
They issued a ``DeprecationWarning`` since 2.0.0,
|
They issued a :py:exc:`DeprecationWarning` since 2.0.0,
|
||||||
an ``Exception`` since 3.0.0
|
an :py:exc:`Exception` since 3.0.0
|
||||||
and ``NotImplementedError`` since 3.3.0.
|
and :py:exc:`NotImplementedError` since 3.3.0.
|
||||||
|
|
||||||
ImageCms.CmsProfile attributes
|
ImageCms.CmsProfile attributes
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -318,7 +318,7 @@ ImageCms.CmsProfile attributes
|
||||||
.. versionremoved:: 8.0.0
|
.. versionremoved:: 8.0.0
|
||||||
|
|
||||||
Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
|
Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
|
||||||
they issued a ``DeprecationWarning``:
|
they issued a :py:exc:`DeprecationWarning`:
|
||||||
|
|
||||||
======================== ===================================================
|
======================== ===================================================
|
||||||
Removed Use instead
|
Removed Use instead
|
||||||
|
@ -442,7 +442,7 @@ PIL.OleFileIO
|
||||||
.. deprecated:: 4.0.0
|
.. deprecated:: 4.0.0
|
||||||
.. versionremoved:: 6.0.0
|
.. versionremoved:: 6.0.0
|
||||||
|
|
||||||
PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
|
``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
|
||||||
the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0
|
the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
|
||||||
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
|
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
|
||||||
PyPI (eg. ``python3 -m pip install olefile``).
|
PyPI (eg. ``python3 -m pip install olefile``).
|
||||||
|
|
|
@ -266,9 +266,12 @@ following options are available::
|
||||||
:py:class:`PIL.ImagePalette.ImagePalette` object.
|
:py:class:`PIL.ImagePalette.ImagePalette` object.
|
||||||
|
|
||||||
**optimize**
|
**optimize**
|
||||||
If present and true, attempt to compress the palette by
|
Whether to attempt to compress the palette by eliminating unused colors.
|
||||||
eliminating unused colors. This is only useful if the palette can
|
This is attempted by default, unless a palette is specified as an option or
|
||||||
be compressed to the next smaller power of 2 elements.
|
as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary.
|
||||||
|
|
||||||
|
This is only useful if the palette can be compressed to the next smaller
|
||||||
|
power of 2 elements.
|
||||||
|
|
||||||
Note that if the image you are saving comes from an existing GIF, it may have
|
Note that if the image you are saving comes from an existing GIF, it may have
|
||||||
the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
|
the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
|
||||||
|
@ -494,6 +497,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
|
|
||||||
If absent, the setting will be determined by libjpeg or libjpeg-turbo.
|
If absent, the setting will be determined by libjpeg or libjpeg-turbo.
|
||||||
|
|
||||||
|
**restart_marker_blocks**
|
||||||
|
If present, emit a restart marker whenever the specified number of MCU
|
||||||
|
blocks has been produced.
|
||||||
|
|
||||||
|
.. versionadded:: 10.2.0
|
||||||
|
|
||||||
|
**restart_marker_rows**
|
||||||
|
If present, emit a restart marker whenever the specified number of MCU
|
||||||
|
rows has been produced.
|
||||||
|
|
||||||
|
.. versionadded:: 10.2.0
|
||||||
|
|
||||||
**qtables**
|
**qtables**
|
||||||
If present, sets the qtables for the encoder. This is listed as an
|
If present, sets the qtables for the encoder. This is listed as an
|
||||||
advanced option for wizards in the JPEG documentation. Use with
|
advanced option for wizards in the JPEG documentation. Use with
|
||||||
|
@ -1296,6 +1311,8 @@ Pillow reads Kodak FlashPix files. In the current version, only the highest
|
||||||
resolution image is read from the file, and the viewing transform is not taken
|
resolution image is read from the file, and the viewing transform is not taken
|
||||||
into account.
|
into account.
|
||||||
|
|
||||||
|
To enable FPX support, you must install :pypi:`olefile`.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
To enable full FlashPix support, you need to build and install the IJG JPEG
|
To enable full FlashPix support, you need to build and install the IJG JPEG
|
||||||
|
@ -1372,6 +1389,8 @@ the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.s
|
||||||
|
|
||||||
Note that there may be an embedded gamma of 2.2 in MIC files.
|
Note that there may be an embedded gamma of 2.2 in MIC files.
|
||||||
|
|
||||||
|
To enable MIC support, you must install :pypi:`olefile`.
|
||||||
|
|
||||||
MPO
|
MPO
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
activateTab(getOS());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
Warnings
|
Warnings
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
@ -42,6 +50,11 @@ Install Pillow with :command:`pip`::
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
python3 -m pip install --upgrade Pillow
|
python3 -m pip install --upgrade Pillow
|
||||||
|
|
||||||
|
Optionally, install :pypi:`defusedxml` for Pillow to read XMP data,
|
||||||
|
and :pypi:`olefile` for Pillow to read FPX and MIC images::
|
||||||
|
|
||||||
|
python3 -m pip install --upgrade defusedxml olefile
|
||||||
|
|
||||||
|
|
||||||
.. tab:: Linux
|
.. tab:: Linux
|
||||||
|
|
||||||
|
@ -146,13 +159,13 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
|
* Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
|
||||||
libjpeg-turbo version **8**.
|
libjpeg-turbo version **8**.
|
||||||
* Starting with Pillow 3.0.0, libjpeg is required by default, but
|
* Starting with Pillow 3.0.0, libjpeg is required by default. It can be
|
||||||
may be disabled with the ``--disable-jpeg`` flag.
|
disabled with the ``-C jpeg=disable`` flag.
|
||||||
|
|
||||||
* **zlib** provides access to compressed PNGs
|
* **zlib** provides access to compressed PNGs
|
||||||
|
|
||||||
* Starting with Pillow 3.0.0, zlib is required by default, but may
|
* Starting with Pillow 3.0.0, zlib is required by default. It can be
|
||||||
be disabled with the ``--disable-zlib`` flag.
|
disabled with the ``-C zlib=disable`` flag.
|
||||||
|
|
||||||
* **libtiff** provides compressed TIFF functionality
|
* **libtiff** provides compressed TIFF functionality
|
||||||
|
|
||||||
|
@ -171,8 +184,6 @@ Many of Pillow's features require external libraries:
|
||||||
transparent WebP files. Versions **0.3.0** and above support
|
transparent WebP files. Versions **0.3.0** and above support
|
||||||
transparency.
|
transparency.
|
||||||
|
|
||||||
* **tcl/tk** provides support for tkinter bitmap and photo images.
|
|
||||||
|
|
||||||
* **openjpeg** provides JPEG 2000 functionality.
|
* **openjpeg** provides JPEG 2000 functionality.
|
||||||
|
|
||||||
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
|
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
|
||||||
|
@ -356,7 +367,7 @@ for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
|
||||||
additional configuration should be required. If they are installed in
|
additional configuration should be required. If they are installed in
|
||||||
a non-standard location, you may need to configure setuptools to use
|
a non-standard location, you may need to configure setuptools to use
|
||||||
those locations by editing :file:`setup.py` or
|
those locations by editing :file:`setup.py` or
|
||||||
:file:`setup.cfg`, or by adding environment variables on the command
|
:file:`pyproject.toml`, or by adding environment variables on the command
|
||||||
line::
|
line::
|
||||||
|
|
||||||
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
|
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
|
||||||
|
@ -456,10 +467,10 @@ These platforms are built and tested for every change.
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Fedora 37 | 3.11 | x86-64 |
|
|
||||||
+----------------------------------+----------------------------+---------------------+
|
|
||||||
| Fedora 38 | 3.11 | x86-64 |
|
| Fedora 38 | 3.11 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
|
| Fedora 39 | 3.12 | x86-64 |
|
||||||
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| Gentoo | 3.9 | x86-64 |
|
| Gentoo | 3.9 | x86-64 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
|
| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
|
||||||
|
@ -478,7 +489,7 @@ These platforms are built and tested for every change.
|
||||||
| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
|
| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
|
||||||
| | 3.12, PyPy3 | |
|
| | 3.12, PyPy3 | |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.11 | x86 |
|
| | 3.12 | x86 |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
| | 3.9 (MinGW) | x86-64 |
|
| | 3.9 (MinGW) | x86-64 |
|
||||||
| +----------------------------+---------------------+
|
| +----------------------------+---------------------+
|
||||||
|
@ -571,6 +582,10 @@ These platforms have been reported to work at the versions mentioned.
|
||||||
+----------------------------------+----------------------------+------------------+--------------+
|
+----------------------------------+----------------------------+------------------+--------------+
|
||||||
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
|
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
|
||||||
+----------------------------------+----------------------------+------------------+--------------+
|
+----------------------------------+----------------------------+------------------+--------------+
|
||||||
|
| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm64 |
|
||||||
|
+----------------------------------+----------------------------+------------------+--------------+
|
||||||
|
| Windows 11 Pro | 3.11, 3.12 | 10.1.0 |x86-64 |
|
||||||
|
+----------------------------------+----------------------------+------------------+--------------+
|
||||||
| Windows 10 | 3.7 | 7.1.0 |x86-64 |
|
| Windows 10 | 3.7 | 7.1.0 |x86-64 |
|
||||||
+----------------------------------+----------------------------+------------------+--------------+
|
+----------------------------------+----------------------------+------------------+--------------+
|
||||||
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
|
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
|
||||||
|
|
|
@ -10,7 +10,7 @@ this class store bitmap fonts, and are used with the
|
||||||
|
|
||||||
PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use
|
PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use
|
||||||
`pilfont.py <https://github.com/python-pillow/pillow-scripts/blob/main/Scripts/pilfont.py>`_
|
`pilfont.py <https://github.com/python-pillow/pillow-scripts/blob/main/Scripts/pilfont.py>`_
|
||||||
from `pillow-scripts <https://pypi.org/project/pillow-scripts/>`_ to convert BDF and
|
from :pypi:`pillow-scripts` to convert BDF and
|
||||||
PCF font descriptors (X window font formats) to this format.
|
PCF font descriptors (X window font formats) to this format.
|
||||||
|
|
||||||
Starting with version 1.1.4, PIL can be configured to support TrueType and
|
Starting with version 1.1.4, PIL can be configured to support TrueType and
|
||||||
|
@ -20,7 +20,7 @@ the imToolkit package.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
To protect against potential DOS attacks when using arbitrary strings as
|
To protect against potential DOS attacks when using arbitrary strings as
|
||||||
text input, Pillow will raise a ``ValueError`` if the number of characters
|
text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
|
||||||
is over a certain limit, :py:data:`MAX_STRING_LENGTH`.
|
is over a certain limit, :py:data:`MAX_STRING_LENGTH`.
|
||||||
|
|
||||||
This threshold can be changed by setting
|
This threshold can be changed by setting
|
||||||
|
@ -89,5 +89,5 @@ Constants
|
||||||
.. data:: MAX_STRING_LENGTH
|
.. data:: MAX_STRING_LENGTH
|
||||||
|
|
||||||
Set to 1,000,000, to protect against potential DOS attacks. Pillow will
|
Set to 1,000,000, to protect against potential DOS attacks. Pillow will
|
||||||
raise a ``ValueError`` if the number of characters is over this limit. The
|
raise a :py:exc:`ValueError` if the number of characters is over this limit. The
|
||||||
check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
|
check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
|
||||||
|
|
|
@ -173,8 +173,8 @@ been processed before Pillow started checking for decompression bombs.
|
||||||
Added ImageFont.MAX_STRING_LENGTH
|
Added ImageFont.MAX_STRING_LENGTH
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
To protect against potential DOS attacks when using arbitrary strings as text
|
:cve:`2023-44271`: To protect against potential DOS attacks when using arbitrary strings as text
|
||||||
input, Pillow will now raise a ``ValueError`` if the number of characters
|
input, Pillow will now raise a :py:exc:`ValueError` if the number of characters
|
||||||
passed into ImageFont methods is over a certain limit,
|
passed into ImageFont methods is over a certain limit,
|
||||||
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
|
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ Setting image mode
|
||||||
^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
If you attempt to set the mode of an image directly, e.g.
|
If you attempt to set the mode of an image directly, e.g.
|
||||||
``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is
|
``im.mode = "RGBA"``, you will now receive an :py:exc:`AttributeError`. This is
|
||||||
not about removing existing functionality, but instead about raising an
|
not about removing existing functionality, but instead about raising an
|
||||||
explicit error to prevent later consequences. The ``convert`` method is the
|
explicit error to prevent later consequences. The ``convert`` method is the
|
||||||
correct way to change an image's mode.
|
correct way to change an image's mode.
|
||||||
|
|
|
@ -10,7 +10,7 @@ operations. As a result PIL was unable to open them as images, requiring a wrap
|
||||||
``cStringIO`` or ``BytesIO``.
|
``cStringIO`` or ``BytesIO``.
|
||||||
|
|
||||||
Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and
|
Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and
|
||||||
catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we
|
catch on exception :py:exc:`AttributeError` or :py:exc:`io.UnsupportedOperation`. If this is caught we
|
||||||
attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like
|
attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like
|
||||||
objects).
|
objects).
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ Deprecation Warning when Saving JPEGs
|
||||||
|
|
||||||
JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0
|
JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0
|
||||||
silently drops the alpha channel. With this release Pillow will now
|
silently drops the alpha channel. With this release Pillow will now
|
||||||
issue a ``DeprecationWarning`` when attempting to save a ``RGBA`` mode
|
issue a :py:exc:`DeprecationWarning` when attempting to save a ``RGBA`` mode
|
||||||
image as a JPEG. This will become an error in Pillow 4.2.
|
image as a JPEG. This will become an error in Pillow 4.2.
|
||||||
|
|
||||||
New DDS Decoders
|
New DDS Decoders
|
||||||
|
|
|
@ -17,8 +17,8 @@ Pillow 4.0 supports Python 3.6.
|
||||||
OleFileIO.py
|
OleFileIO.py
|
||||||
============
|
============
|
||||||
|
|
||||||
OleFileIO.py has been removed as a vendored file and is now installed
|
``OleFileIO.py`` has been removed as a vendored file and is now installed
|
||||||
from the upstream olefile pypi package. All internal dependencies are
|
from the upstream :pypi:`olefile` PyPI package. All internal dependencies are
|
||||||
redirected to the olefile package. Direct accesses to
|
redirected to the olefile package. Direct accesses to
|
||||||
``PIL.OlefileIO`` raises a deprecation warning, then patches the
|
``PIL.OlefileIO`` raises a deprecation warning, then patches the
|
||||||
upstream olefile into ``sys.modules`` in its place.
|
upstream olefile into ``sys.modules`` in its place.
|
||||||
|
|
|
@ -28,7 +28,7 @@ Scripts
|
||||||
|
|
||||||
The scripts formerly installed by Pillow have been split into a
|
The scripts formerly installed by Pillow have been split into a
|
||||||
separate package, pillow-scripts, living at
|
separate package, pillow-scripts, living at
|
||||||
https://github.com/python-pillow/pillow-scripts .
|
https://github.com/python-pillow/pillow-scripts.
|
||||||
|
|
||||||
|
|
||||||
API Changes
|
API Changes
|
||||||
|
@ -37,7 +37,7 @@ API Changes
|
||||||
OleFileIO.py
|
OleFileIO.py
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
The olefile module is no longer a required dependency when installing Pillow.
|
The :pypi:`olefile` module is no longer a required dependency when installing Pillow.
|
||||||
Support for plugins requiring olefile will not be loaded if it is not
|
Support for plugins requiring olefile will not be loaded if it is not
|
||||||
installed. This allows library consumers to avoid installing this dependency
|
installed. This allows library consumers to avoid installing this dependency
|
||||||
if they choose. Some library consumers have little interest in the format
|
if they choose. Some library consumers have little interest in the format
|
||||||
|
|
|
@ -8,7 +8,7 @@ Image size
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
|
|
||||||
If you attempt to set the size of an image directly, e.g.
|
If you attempt to set the size of an image directly, e.g.
|
||||||
``im.size = (100, 100)``, you will now receive an ``AttributeError``. This is
|
``im.size = (100, 100)``, you will now receive an :py:exc:`AttributeError`. This is
|
||||||
not about removing existing functionality, but instead about raising an
|
not about removing existing functionality, but instead about raising an
|
||||||
explicit error to prevent later consequences. The ``resize`` method is the
|
explicit error to prevent later consequences. The ``resize`` method is the
|
||||||
correct way to change an image's size.
|
correct way to change an image's size.
|
||||||
|
@ -16,7 +16,8 @@ correct way to change an image's size.
|
||||||
The exceptions to this are:
|
The exceptions to this are:
|
||||||
|
|
||||||
* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage.
|
* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage.
|
||||||
* The TIFF image format, which now has a ``DeprecationWarning`` for this action, as direct image size setting was previously necessary to work around an issue with tile extents.
|
* The TIFF image format, which now has a :py:exc:`DeprecationWarning` for this action,
|
||||||
|
as direct image size setting was previously necessary to work around an issue with tile extents.
|
||||||
|
|
||||||
|
|
||||||
API Additions
|
API Additions
|
||||||
|
|
|
@ -15,7 +15,7 @@ PNG: Handle IDAT chunks after image end
|
||||||
|
|
||||||
Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop
|
Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop
|
||||||
reading image data before the IDAT chunks finish. A regression caused an
|
reading image data before the IDAT chunks finish. A regression caused an
|
||||||
``EOFError`` exception when previously there was none. This is now fixed, and
|
:py:exc:`EOFError` exception when previously there was none. This is now fixed, and
|
||||||
file reading continues in case there are subsequent text chunks.
|
file reading continues in case there are subsequent text chunks.
|
||||||
|
|
||||||
PNG: MIME type
|
PNG: MIME type
|
||||||
|
@ -30,7 +30,7 @@ File closing
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
A regression caused an unsupported image file to report a
|
A regression caused an unsupported image file to report a
|
||||||
``ValueError: seek of closed file`` exception instead of an ``OSError``. This
|
``ValueError: seek of closed file`` exception instead of an :py:exc:`OSError`. This
|
||||||
has been fixed by ensuring that image plugins only close their internal ``__fp``
|
has been fixed by ensuring that image plugins only close their internal ``__fp``
|
||||||
if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own
|
if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own
|
||||||
file pointers.
|
file pointers.
|
||||||
|
|
|
@ -14,8 +14,8 @@ Pillow for Python 3.4 is 5.4.1.
|
||||||
Removed deprecated PIL.OleFileIO
|
Removed deprecated PIL.OleFileIO
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
|
``PIL.OleFileIO`` was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
|
||||||
the upstream olefile Python package, and replaced with an ``ImportError``. The
|
the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError`. The
|
||||||
deprecated file has now been removed from Pillow. If needed, install from PyPI (eg.
|
deprecated file has now been removed from Pillow. If needed, install from PyPI (eg.
|
||||||
``python3 -m pip install olefile``).
|
``python3 -m pip install olefile``).
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ ImageCms.CmsProfile attributes
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From
|
Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From
|
||||||
6.0.0, they issue a ``DeprecationWarning``:
|
6.0.0, they issue a :py:exc:`DeprecationWarning`:
|
||||||
|
|
||||||
======================== ===============================
|
======================== ===============================
|
||||||
Deprecated Use instead
|
Deprecated Use instead
|
||||||
|
|
|
@ -58,7 +58,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods,
|
||||||
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and
|
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and
|
||||||
:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and
|
:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and
|
||||||
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes
|
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes
|
||||||
instead. An ``IOError`` will be raised if the font is not a variation font. FreeType
|
instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType
|
||||||
2.9.1 or greater is required.
|
2.9.1 or greater is required.
|
||||||
|
|
||||||
Other Changes
|
Other Changes
|
||||||
|
|
|
@ -85,7 +85,7 @@ Custom unidentified image error
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
|
Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
|
||||||
identified. For backwards compatibility, this will inherit from ``OSError``.
|
identified. For backwards compatibility, this will inherit from :py:exc:`OSError`.
|
||||||
|
|
||||||
New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods
|
New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
@ -7,7 +7,7 @@ Fix another regression seeking PNG files
|
||||||
This fixes a regression introduced in 7.1.0 when adding support for APNG files.
|
This fixes a regression introduced in 7.1.0 when adding support for APNG files.
|
||||||
|
|
||||||
When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an
|
When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an
|
||||||
``EOFError`` as it should have done, resulting in:
|
:py:exc:`EOFError` as it should have done, resulting in:
|
||||||
|
|
||||||
.. code-block:: pycon
|
.. code-block:: pycon
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,6 @@ a custom :py:class:`~PIL.ImageShow.Viewer` class.
|
||||||
ImageFile.raise_ioerror
|
ImageFile.raise_ioerror
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror``
|
:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror``
|
||||||
is now deprecated and will be removed in a future release. Use
|
is now deprecated and will be removed in a future release. Use
|
||||||
``ImageFile.raise_oserror`` instead.
|
``ImageFile.raise_oserror`` instead.
|
||||||
|
|
|
@ -168,7 +168,7 @@ offset.
|
||||||
Error for large BMP files
|
Error for large BMP files
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Previously, if a BMP file was too large, an ``OSError`` would be raised. Now,
|
Previously, if a BMP file was too large, an :py:exc:`OSError` would be raised. Now,
|
||||||
``DecompressionBombError`` is used instead, as Pillow already uses for other formats.
|
``DecompressionBombError`` is used instead, as Pillow already uses for other formats.
|
||||||
|
|
||||||
Dark theme for docs
|
Dark theme for docs
|
||||||
|
|
|
@ -22,9 +22,10 @@ Catch OSError when checking if destination is sys.stdout
|
||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was
|
In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was
|
||||||
updated. This lead to an OSError being raised if the environment restricted access.
|
updated. This lead to an :py:exc:`OSError` being raised if the environment restricted
|
||||||
|
access.
|
||||||
|
|
||||||
The OSError is now silently caught.
|
The :py:exc:`OSError` is now silently caught.
|
||||||
|
|
||||||
Fixed removing orientation in ImageOps.exif_transpose
|
Fixed removing orientation in ImageOps.exif_transpose
|
||||||
=====================================================
|
=====================================================
|
||||||
|
@ -34,7 +35,7 @@ original image EXIF data was not modified, and the orientation was only removed
|
||||||
the modified copy.
|
the modified copy.
|
||||||
|
|
||||||
However, for certain images the orientation was already missing from the modified
|
However, for certain images the orientation was already missing from the modified
|
||||||
image, leading to a KeyError.
|
image, leading to a :py:exc:`KeyError`.
|
||||||
|
|
||||||
This error has been resolved, and the copying of metadata to the modified image
|
This error has been resolved, and the copying of metadata to the modified image
|
||||||
improved.
|
improved.
|
||||||
|
|
|
@ -63,7 +63,7 @@ a custom :py:class:`~PIL.ImageShow.Viewer` class.
|
||||||
ImageFile.raise_ioerror
|
ImageFile.raise_ioerror
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror``
|
:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3. So, ``ImageFile.raise_ioerror``
|
||||||
has been removed. Use ``ImageFile.raise_oserror`` instead.
|
has been removed. Use ``ImageFile.raise_oserror`` instead.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,14 @@ Raise an error when performing a negative crop
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now
|
Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now
|
||||||
it will raise a ``ValueError``, to help reduce confusion if a user has unintentionally
|
it will raise a :py:exc:`ValueError`, to help reduce confusion if a user has unintentionally
|
||||||
provided the wrong arguments.
|
provided the wrong arguments.
|
||||||
|
|
||||||
Added specific error if path coordinate type is incorrect
|
Added specific error if path coordinate type is incorrect
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Rather than returning a ``SystemError``, passing the incorrect types of coordinates into
|
Rather than returning a :py:exc:`SystemError`, passing the incorrect types of coordinates into
|
||||||
a path will now raise a more specific ``ValueError``, with the message "incorrect
|
a path will now raise a more specific :py:exc:`ValueError`, with the message "incorrect
|
||||||
coordinate type".
|
coordinate type".
|
||||||
|
|
||||||
Replace requirements.txt with extras
|
Replace requirements.txt with extras
|
||||||
|
|
36
docs/resources/js/activate_tab.js
Normal file
36
docs/resources/js/activate_tab.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Based on https://stackoverflow.com/a/38241481/724176
|
||||||
|
function getOS() {
|
||||||
|
const userAgent = window.navigator.userAgent,
|
||||||
|
platform = window.navigator.userAgentData?.platform || window.navigator.platform,
|
||||||
|
macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"],
|
||||||
|
windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
|
||||||
|
|
||||||
|
if (macosPlatforms.includes(platform)) {
|
||||||
|
return "macOS";
|
||||||
|
} else if (windowsPlatforms.includes(platform)) {
|
||||||
|
return "Windows";
|
||||||
|
} else if (/Android/.test(userAgent)) {
|
||||||
|
return "Android";
|
||||||
|
} else if (/Linux/.test(platform)) {
|
||||||
|
return "Linux";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(tabName) {
|
||||||
|
// Find all label elements with the specified tab name
|
||||||
|
const labels = document.querySelectorAll(".tab-label");
|
||||||
|
|
||||||
|
labels.forEach((label) => {
|
||||||
|
if (label.textContent == tabName) {
|
||||||
|
// Find the associated input element using the "for" attribute
|
||||||
|
const tabInputId = label.getAttribute("for");
|
||||||
|
const tabInput = document.getElementById(tabInputId);
|
||||||
|
|
||||||
|
// Check if the input element exists before attempting to set the "checked" attribute
|
||||||
|
if (tabInput) {
|
||||||
|
// Activate the tab by setting its "checked" attribute to true
|
||||||
|
tabInput.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
119
pyproject.toml
119
pyproject.toml
|
@ -6,3 +6,122 @@ requires = [
|
||||||
backend-path = [
|
backend-path = [
|
||||||
"_custom_build",
|
"_custom_build",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "pillow"
|
||||||
|
description = "Python Imaging Library (Fork)"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = [
|
||||||
|
"Imaging",
|
||||||
|
]
|
||||||
|
license = {text = "HPND"}
|
||||||
|
authors = [{name = "Jeffrey A. Clark (Alex)", email = "aclark@aclark.net"}]
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 6 - Mature",
|
||||||
|
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
"Topic :: Multimedia :: Graphics",
|
||||||
|
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
|
||||||
|
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
|
||||||
|
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
||||||
|
"Topic :: Multimedia :: Graphics :: Viewers",
|
||||||
|
]
|
||||||
|
dynamic = [
|
||||||
|
"version",
|
||||||
|
]
|
||||||
|
[project.optional-dependencies]
|
||||||
|
docs = [
|
||||||
|
"furo",
|
||||||
|
"olefile",
|
||||||
|
"sphinx>=2.4",
|
||||||
|
"sphinx-copybutton",
|
||||||
|
"sphinx-inline-tabs",
|
||||||
|
"sphinx-removed-in",
|
||||||
|
"sphinxext-opengraph",
|
||||||
|
]
|
||||||
|
fpx = [
|
||||||
|
"olefile",
|
||||||
|
]
|
||||||
|
mic = [
|
||||||
|
"olefile",
|
||||||
|
]
|
||||||
|
tests = [
|
||||||
|
"check-manifest",
|
||||||
|
"coverage",
|
||||||
|
"defusedxml",
|
||||||
|
"markdown2",
|
||||||
|
"olefile",
|
||||||
|
"packaging",
|
||||||
|
"pyroma",
|
||||||
|
"pytest",
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest-timeout",
|
||||||
|
]
|
||||||
|
xmp = [
|
||||||
|
"defusedxml",
|
||||||
|
]
|
||||||
|
[project.urls]
|
||||||
|
Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst"
|
||||||
|
Documentation = "https://pillow.readthedocs.io"
|
||||||
|
Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
|
||||||
|
Homepage = "https://python-pillow.org"
|
||||||
|
Mastodon = "https://fosstodon.org/@pillow"
|
||||||
|
"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
|
||||||
|
Source = "https://github.com/python-pillow/Pillow"
|
||||||
|
Twitter = "https://twitter.com/PythonPillow"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["PIL"]
|
||||||
|
include-package-data = true
|
||||||
|
package-dir = {"" = "src"}
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "PIL.__version__"}
|
||||||
|
|
||||||
|
[tool.cibuildwheel]
|
||||||
|
before-all = ".github/workflows/wheels-dependencies.sh"
|
||||||
|
build-verbosity = 1
|
||||||
|
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
|
||||||
|
test-command = "cd {project} && .github/workflows/wheels-test.sh"
|
||||||
|
test-extras = "tests"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 88
|
||||||
|
select = [
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"EM", # flake8-errmsg
|
||||||
|
"F", # pyflakes errors
|
||||||
|
"I", # isort
|
||||||
|
"ISC", # flake8-implicit-str-concat
|
||||||
|
"PGH", # pygrep-hooks
|
||||||
|
"RUF100", # unused noqa (yesqa)
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"YTT", # flake8-2020
|
||||||
|
# "LOG", # TODO: enable flake8-logging when it's not in preview anymore
|
||||||
|
]
|
||||||
|
extend-ignore = [
|
||||||
|
"E203", # Whitespace before ':'
|
||||||
|
"E221", # Multiple spaces before operator
|
||||||
|
"E226", # Missing whitespace around arithmetic operator
|
||||||
|
"E241", # Multiple spaces after ','
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.per-file-ignores]
|
||||||
|
"Tests/*.py" = ["I001"]
|
||||||
|
|
||||||
|
[tool.ruff.isort]
|
||||||
|
known-first-party = ["PIL"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-ra --color=yes"
|
||||||
|
testpaths = ["Tests"]
|
||||||
|
|
74
setup.cfg
74
setup.cfg
|
@ -1,74 +0,0 @@
|
||||||
[metadata]
|
|
||||||
name = Pillow
|
|
||||||
description = Python Imaging Library (Fork)
|
|
||||||
long_description = file: README.md
|
|
||||||
long_description_content_type = text/markdown
|
|
||||||
url = https://python-pillow.org
|
|
||||||
author = Jeffrey A. Clark (Alex)
|
|
||||||
author_email = aclark@aclark.net
|
|
||||||
license = HPND
|
|
||||||
classifiers =
|
|
||||||
Development Status :: 6 - Mature
|
|
||||||
License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3 :: Only
|
|
||||||
Programming Language :: Python :: 3.8
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
Programming Language :: Python :: 3.10
|
|
||||||
Programming Language :: Python :: 3.11
|
|
||||||
Programming Language :: Python :: 3.12
|
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
|
||||||
Programming Language :: Python :: Implementation :: PyPy
|
|
||||||
Topic :: Multimedia :: Graphics
|
|
||||||
Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
|
|
||||||
Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
|
|
||||||
Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
||||||
Topic :: Multimedia :: Graphics :: Viewers
|
|
||||||
keywords = Imaging
|
|
||||||
project_urls =
|
|
||||||
Documentation=https://pillow.readthedocs.io
|
|
||||||
Source=https://github.com/python-pillow/Pillow
|
|
||||||
Funding=https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi
|
|
||||||
Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html
|
|
||||||
Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
|
|
||||||
Twitter=https://twitter.com/PythonPillow
|
|
||||||
Mastodon=https://fosstodon.org/@pillow
|
|
||||||
|
|
||||||
[options]
|
|
||||||
packages = PIL
|
|
||||||
python_requires = >=3.8
|
|
||||||
include_package_data = True
|
|
||||||
package_dir =
|
|
||||||
= src
|
|
||||||
|
|
||||||
[options.extras_require]
|
|
||||||
docs =
|
|
||||||
furo
|
|
||||||
olefile
|
|
||||||
sphinx>=2.4
|
|
||||||
sphinx-copybutton
|
|
||||||
sphinx-inline-tabs
|
|
||||||
sphinx-removed-in
|
|
||||||
sphinxext-opengraph
|
|
||||||
tests =
|
|
||||||
check-manifest
|
|
||||||
coverage
|
|
||||||
defusedxml
|
|
||||||
markdown2
|
|
||||||
olefile
|
|
||||||
packaging
|
|
||||||
pyroma
|
|
||||||
pytest
|
|
||||||
pytest-cov
|
|
||||||
pytest-timeout
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
extend-ignore = E203
|
|
||||||
max-line-length = 88
|
|
||||||
|
|
||||||
[isort]
|
|
||||||
profile = black
|
|
||||||
|
|
||||||
[tool:pytest]
|
|
||||||
addopts = -ra --color=yes
|
|
||||||
testpaths = Tests
|
|
23
setup.py
23
setup.py
|
@ -440,17 +440,17 @@ class pil_build_ext(build_ext):
|
||||||
|
|
||||||
#
|
#
|
||||||
# add configured kits
|
# add configured kits
|
||||||
for root_name, lib_name in dict(
|
for root_name, lib_name in {
|
||||||
JPEG_ROOT="libjpeg",
|
"JPEG_ROOT": "libjpeg",
|
||||||
JPEG2K_ROOT="libopenjp2",
|
"JPEG2K_ROOT": "libopenjp2",
|
||||||
TIFF_ROOT=("libtiff-5", "libtiff-4"),
|
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),
|
||||||
ZLIB_ROOT="zlib",
|
"ZLIB_ROOT": "zlib",
|
||||||
FREETYPE_ROOT="freetype2",
|
"FREETYPE_ROOT": "freetype2",
|
||||||
HARFBUZZ_ROOT="harfbuzz",
|
"HARFBUZZ_ROOT": "harfbuzz",
|
||||||
FRIBIDI_ROOT="fribidi",
|
"FRIBIDI_ROOT": "fribidi",
|
||||||
LCMS_ROOT="lcms2",
|
"LCMS_ROOT": "lcms2",
|
||||||
IMAGEQUANT_ROOT="libimagequant",
|
"IMAGEQUANT_ROOT": "libimagequant",
|
||||||
).items():
|
}.items():
|
||||||
root = globals()[root_name]
|
root = globals()[root_name]
|
||||||
|
|
||||||
if root is None and root_name in os.environ:
|
if root is None and root_name in os.environ:
|
||||||
|
@ -986,7 +986,6 @@ ext_modules = [
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setup(
|
setup(
|
||||||
version=PILLOW_VERSION,
|
|
||||||
cmdclass={"build_ext": pil_build_ext},
|
cmdclass={"build_ext": pil_build_ext},
|
||||||
ext_modules=ext_modules,
|
ext_modules=ext_modules,
|
||||||
zip_safe=not (debug_build() or PLATFORM_MINGW),
|
zip_safe=not (debug_build() or PLATFORM_MINGW),
|
||||||
|
|
|
@ -396,7 +396,7 @@ def _save(im, fp, filename, bitmap_header=True):
|
||||||
dpi = info.get("dpi", (96, 96))
|
dpi = info.get("dpi", (96, 96))
|
||||||
|
|
||||||
# 1 meter == 39.3701 inches
|
# 1 meter == 39.3701 inches
|
||||||
ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
|
ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
|
||||||
|
|
||||||
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
|
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
|
||||||
header = 40 # or 64 for OS/2 version 2
|
header = 40 # or 64 for OS/2 version 2
|
||||||
|
|
|
@ -64,8 +64,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
||||||
d, e, o, a = self.tile[0]
|
d, e, o, a = self.tile[0]
|
||||||
self.tile[0] = d, (0, 0) + self.size, o, a
|
self.tile[0] = d, (0, 0) + self.size, o, a
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -77,14 +77,11 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||||
|
|
||||||
# Hack to support hi-res rendering
|
# Hack to support hi-res rendering
|
||||||
scale = int(scale) or 1
|
scale = int(scale) or 1
|
||||||
# orig_size = size
|
width = size[0] * scale
|
||||||
# orig_bbox = bbox
|
height = size[1] * scale
|
||||||
size = (size[0] * scale, size[1] * scale)
|
|
||||||
# resolution is dependent on bbox and size
|
# resolution is dependent on bbox and size
|
||||||
res = (
|
res_x = 72.0 * width / (bbox[2] - bbox[0])
|
||||||
72.0 * size[0] / (bbox[2] - bbox[0]),
|
res_y = 72.0 * height / (bbox[3] - bbox[1])
|
||||||
72.0 * size[1] / (bbox[3] - bbox[1]),
|
|
||||||
)
|
|
||||||
|
|
||||||
out_fd, outfile = tempfile.mkstemp()
|
out_fd, outfile = tempfile.mkstemp()
|
||||||
os.close(out_fd)
|
os.close(out_fd)
|
||||||
|
@ -121,8 +118,8 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||||
command = [
|
command = [
|
||||||
gs_binary,
|
gs_binary,
|
||||||
"-q", # quiet mode
|
"-q", # quiet mode
|
||||||
"-g%dx%d" % size, # set output geometry (pixels)
|
f"-g{width:d}x{height:d}", # set output geometry (pixels)
|
||||||
"-r%fx%f" % res, # set input DPI (dots per inch)
|
f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch)
|
||||||
"-dBATCH", # exit after processing
|
"-dBATCH", # exit after processing
|
||||||
"-dNOPAUSE", # don't pause between pages
|
"-dNOPAUSE", # don't pause between pages
|
||||||
"-dSAFER", # safe mode
|
"-dSAFER", # safe mode
|
||||||
|
|
|
@ -54,12 +54,10 @@ class FitsImageFile(ImageFile.ImageFile):
|
||||||
self._mode = "L"
|
self._mode = "L"
|
||||||
elif number_of_bits == 16:
|
elif number_of_bits == 16:
|
||||||
self._mode = "I"
|
self._mode = "I"
|
||||||
# rawmode = "I;16S"
|
|
||||||
elif number_of_bits == 32:
|
elif number_of_bits == 32:
|
||||||
self._mode = "I"
|
self._mode = "I"
|
||||||
elif number_of_bits in (-32, -64):
|
elif number_of_bits in (-32, -64):
|
||||||
self._mode = "F"
|
self._mode = "F"
|
||||||
# rawmode = "F" if number_of_bits == -32 else "F;64F"
|
|
||||||
|
|
||||||
offset = math.ceil(self.fp.tell() / 2880) * 2880
|
offset = math.ceil(self.fp.tell() / 2880) * 2880
|
||||||
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
|
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
|
||||||
|
|
|
@ -78,7 +78,6 @@ class FontFile:
|
||||||
if glyph:
|
if glyph:
|
||||||
d, dst, src, im = glyph
|
d, dst, src, im = glyph
|
||||||
xx = src[2] - src[0]
|
xx = src[2] - src[0]
|
||||||
# yy = src[3] - src[1]
|
|
||||||
x0, y0 = x, y
|
x0, y0 = x, y
|
||||||
x = x + xx
|
x = x + xx
|
||||||
if x > WIDTH:
|
if x > WIDTH:
|
||||||
|
|
|
@ -227,6 +227,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
||||||
break # isn't really required
|
break # isn't really required
|
||||||
|
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
|
self._fp = self.fp
|
||||||
self.fp = None
|
self.fp = None
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
|
|
|
@ -281,14 +281,9 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
bits = self.fp.read(1)[0]
|
bits = self.fp.read(1)[0]
|
||||||
self.__offset = self.fp.tell()
|
self.__offset = self.fp.tell()
|
||||||
break
|
break
|
||||||
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
# raise OSError, "illegal GIF tag `%x`" % s[0]
|
|
||||||
s = None
|
s = None
|
||||||
|
|
||||||
if interlace is None:
|
if interlace is None:
|
||||||
# self._fp = None
|
|
||||||
msg = "image not found in GIF frame"
|
msg = "image not found in GIF frame"
|
||||||
raise EOFError(msg)
|
raise EOFError(msg)
|
||||||
|
|
||||||
|
@ -661,7 +656,7 @@ def _save(im, fp, filename, save_all=False):
|
||||||
palette = im.encoderinfo.get("palette", im.info.get("palette"))
|
palette = im.encoderinfo.get("palette", im.info.get("palette"))
|
||||||
else:
|
else:
|
||||||
palette = None
|
palette = None
|
||||||
im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)
|
im.encoderinfo.setdefault("optimize", True)
|
||||||
|
|
||||||
if not save_all or not _write_multiple_frames(im, fp, palette):
|
if not save_all or not _write_multiple_frames(im, fp, palette):
|
||||||
_write_single_frame(im, fp, palette)
|
_write_single_frame(im, fp, palette)
|
||||||
|
|
|
@ -391,8 +391,8 @@ if __name__ == "__main__":
|
||||||
with open(sys.argv[1], "rb") as fp:
|
with open(sys.argv[1], "rb") as fp:
|
||||||
imf = IcnsImageFile(fp)
|
imf = IcnsImageFile(fp)
|
||||||
for size in imf.info["sizes"]:
|
for size in imf.info["sizes"]:
|
||||||
imf.size = size
|
width, height, scale = imf.size = size
|
||||||
imf.save("out-%s-%s-%s.png" % size)
|
imf.save(f"out-{width}-{height}-{scale}.png")
|
||||||
with Image.open(sys.argv[1]) as im:
|
with Image.open(sys.argv[1]) as im:
|
||||||
im.save("out.png")
|
im.save("out.png")
|
||||||
if sys.platform == "windows":
|
if sys.platform == "windows":
|
||||||
|
|
|
@ -174,9 +174,7 @@ class IcoFile:
|
||||||
|
|
||||||
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
|
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
|
||||||
# ICO images are usually squares
|
# ICO images are usually squares
|
||||||
# self.entry = sorted(self.entry, key=lambda x: x['width'])
|
self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
|
||||||
self.entry = sorted(self.entry, key=lambda x: x["square"])
|
|
||||||
self.entry.reverse()
|
|
||||||
|
|
||||||
def sizes(self):
|
def sizes(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -42,7 +42,7 @@ from pathlib import Path
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import defusedxml.ElementTree as ElementTree
|
from defusedxml import ElementTree
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ElementTree = None
|
ElementTree = None
|
||||||
|
|
||||||
|
@ -558,16 +558,17 @@ class Image:
|
||||||
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
|
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
|
||||||
more information.
|
more information.
|
||||||
"""
|
"""
|
||||||
try:
|
if hasattr(self, "fp"):
|
||||||
if getattr(self, "_fp", False):
|
try:
|
||||||
if self._fp != self.fp:
|
if getattr(self, "_fp", False):
|
||||||
self._fp.close()
|
if self._fp != self.fp:
|
||||||
self._fp = DeferredError(ValueError("Operation on closed image"))
|
self._fp.close()
|
||||||
if self.fp:
|
self._fp = DeferredError(ValueError("Operation on closed image"))
|
||||||
self.fp.close()
|
if self.fp:
|
||||||
self.fp = None
|
self.fp.close()
|
||||||
except Exception as msg:
|
self.fp = None
|
||||||
logger.debug("Error closing: %s", msg)
|
except Exception as msg:
|
||||||
|
logger.debug("Error closing: %s", msg)
|
||||||
|
|
||||||
if getattr(self, "map", None):
|
if getattr(self, "map", None):
|
||||||
self.map = None
|
self.map = None
|
||||||
|
@ -801,6 +802,9 @@ class Image:
|
||||||
but loads data into this image instead of creating a new image object.
|
but loads data into this image instead of creating a new image object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if self.width == 0 or self.height == 0:
|
||||||
|
return
|
||||||
|
|
||||||
# may pass tuple instead of argument list
|
# may pass tuple instead of argument list
|
||||||
if len(args) == 1 and isinstance(args[0], tuple):
|
if len(args) == 1 and isinstance(args[0], tuple):
|
||||||
args = args[0]
|
args = args[0]
|
||||||
|
@ -1166,7 +1170,7 @@ class Image:
|
||||||
if palette.mode != "P":
|
if palette.mode != "P":
|
||||||
msg = "bad mode for palette image"
|
msg = "bad mode for palette image"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
if self.mode != "RGB" and self.mode != "L":
|
if self.mode not in {"RGB", "L"}:
|
||||||
msg = "only RGB or L mode images can be quantized to a palette"
|
msg = "only RGB or L mode images can be quantized to a palette"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
im = self.im.convert("P", dither, palette.im)
|
im = self.im.convert("P", dither, palette.im)
|
||||||
|
@ -2977,15 +2981,16 @@ def frombytes(mode, size, data, decoder_name="raw", *args):
|
||||||
|
|
||||||
_check_size(size)
|
_check_size(size)
|
||||||
|
|
||||||
# may pass tuple instead of argument list
|
|
||||||
if len(args) == 1 and isinstance(args[0], tuple):
|
|
||||||
args = args[0]
|
|
||||||
|
|
||||||
if decoder_name == "raw" and args == ():
|
|
||||||
args = mode
|
|
||||||
|
|
||||||
im = new(mode, size)
|
im = new(mode, size)
|
||||||
im.frombytes(data, decoder_name, args)
|
if im.width != 0 and im.height != 0:
|
||||||
|
# may pass tuple instead of argument list
|
||||||
|
if len(args) == 1 and isinstance(args[0], tuple):
|
||||||
|
args = args[0]
|
||||||
|
|
||||||
|
if decoder_name == "raw" and args == ():
|
||||||
|
args = mode
|
||||||
|
|
||||||
|
im.frombytes(data, decoder_name, args)
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
|
@ -3106,7 +3111,8 @@ def fromarray(obj, mode=None):
|
||||||
try:
|
try:
|
||||||
mode, rawmode = _fromarray_typemap[typekey]
|
mode, rawmode = _fromarray_typemap[typekey]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
msg = "Cannot handle this data type: %s, %s" % typekey
|
typekey_shape, typestr = typekey
|
||||||
|
msg = f"Cannot handle this data type: {typekey_shape}, {typestr}"
|
||||||
raise TypeError(msg) from e
|
raise TypeError(msg) from e
|
||||||
else:
|
else:
|
||||||
rawmode = mode
|
rawmode = mode
|
||||||
|
|
|
@ -921,7 +921,7 @@ def floodfill(image, xy, value, border=None, thresh=0):
|
||||||
if border is None:
|
if border is None:
|
||||||
fill = _color_diff(p, background) <= thresh
|
fill = _color_diff(p, background) <= thresh
|
||||||
else:
|
else:
|
||||||
fill = p != value and p != border
|
fill = p not in (value, border)
|
||||||
if fill:
|
if fill:
|
||||||
pixel[s, t] = value
|
pixel[s, t] = value
|
||||||
new_edge.add((s, t))
|
new_edge.add((s, t))
|
||||||
|
|
|
@ -431,7 +431,6 @@ class Parser:
|
||||||
with io.BytesIO(self.data) as fp:
|
with io.BytesIO(self.data) as fp:
|
||||||
im = Image.open(fp)
|
im = Image.open(fp)
|
||||||
except OSError:
|
except OSError:
|
||||||
# traceback.print_exc()
|
|
||||||
pass # not enough data
|
pass # not enough data
|
||||||
else:
|
else:
|
||||||
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
|
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
|
||||||
|
|
|
@ -222,7 +222,7 @@ class UnsharpMask(MultibandFilter):
|
||||||
|
|
||||||
.. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
|
.. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
|
||||||
|
|
||||||
""" # noqa: E501
|
"""
|
||||||
|
|
||||||
name = "UnsharpMask"
|
name = "UnsharpMask"
|
||||||
|
|
||||||
|
|
|
@ -788,8 +788,13 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
||||||
.. versionadded:: 4.2.0
|
.. versionadded:: 4.2.0
|
||||||
:return: A font object.
|
:return: A font object.
|
||||||
:exception OSError: If the file could not be read.
|
:exception OSError: If the file could not be read.
|
||||||
|
:exception ValueError: If the font size is not greater than zero.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if size <= 0:
|
||||||
|
msg = "font size must be greater than 0"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
def freetype(font):
|
def freetype(font):
|
||||||
return FreeTypeFont(font, size, index, encoding, layout_engine)
|
return FreeTypeFont(font, size, index, encoding, layout_engine)
|
||||||
|
|
||||||
|
|
|
@ -239,7 +239,7 @@ def eval(expression, _dict={}, **kw):
|
||||||
args = ops.copy()
|
args = ops.copy()
|
||||||
args.update(_dict)
|
args.update(_dict)
|
||||||
args.update(kw)
|
args.update(kw)
|
||||||
for k, v in list(args.items()):
|
for k, v in args.items():
|
||||||
if hasattr(v, "im"):
|
if hasattr(v, "im"):
|
||||||
args[k] = _Operand(v)
|
args[k] = _Operand(v)
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ def _lut(image, lut):
|
||||||
lut = lut + lut + lut
|
lut = lut + lut + lut
|
||||||
return image.point(lut)
|
return image.point(lut)
|
||||||
else:
|
else:
|
||||||
msg = "not supported for this image mode"
|
msg = f"not supported for mode {image.mode}"
|
||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -257,8 +257,6 @@ def load(filename):
|
||||||
if lut:
|
if lut:
|
||||||
break
|
break
|
||||||
except (SyntaxError, ValueError):
|
except (SyntaxError, ValueError):
|
||||||
# import traceback
|
|
||||||
# traceback.print_exc()
|
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
msg = "cannot load palette"
|
msg = "cannot load palette"
|
||||||
|
|
|
@ -83,16 +83,6 @@ def fromqimage(im):
|
||||||
|
|
||||||
def fromqpixmap(im):
|
def fromqpixmap(im):
|
||||||
return fromqimage(im)
|
return fromqimage(im)
|
||||||
# buffer = QBuffer()
|
|
||||||
# buffer.open(QIODevice.ReadWrite)
|
|
||||||
# # im.save(buffer)
|
|
||||||
# # What if png doesn't support some image features like animation?
|
|
||||||
# im.save(buffer, 'ppm')
|
|
||||||
# bytes_io = BytesIO()
|
|
||||||
# bytes_io.write(buffer.data())
|
|
||||||
# buffer.close()
|
|
||||||
# bytes_io.seek(0)
|
|
||||||
# return Image.open(bytes_io)
|
|
||||||
|
|
||||||
|
|
||||||
def align8to32(bytes, width, mode):
|
def align8to32(bytes, width, mode):
|
||||||
|
@ -208,9 +198,5 @@ def toqimage(im):
|
||||||
|
|
||||||
|
|
||||||
def toqpixmap(im):
|
def toqpixmap(im):
|
||||||
# # This doesn't work. For now using a dumb approach.
|
|
||||||
# im_data = _toqclass_helper(im)
|
|
||||||
# result = QPixmap(im_data["size"][0], im_data["size"][1])
|
|
||||||
# result.loadFromData(im_data["data"])
|
|
||||||
qimage = toqimage(im)
|
qimage = toqimage(im)
|
||||||
return QPixmap.fromImage(qimage)
|
return QPixmap.fromImage(qimage)
|
||||||
|
|
|
@ -18,10 +18,9 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
from ._binary import i8
|
from ._binary import i8, o8
|
||||||
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
|
|
||||||
|
|
||||||
COMPRESSION = {1: "raw", 5: "jpeg"}
|
COMPRESSION = {1: "raw", 5: "jpeg"}
|
||||||
|
|
||||||
|
|
|
@ -334,10 +334,7 @@ def _save(im, fp, filename):
|
||||||
if quality_layers is not None and not (
|
if quality_layers is not None and not (
|
||||||
isinstance(quality_layers, (list, tuple))
|
isinstance(quality_layers, (list, tuple))
|
||||||
and all(
|
and all(
|
||||||
[
|
isinstance(quality_layer, (int, float)) for quality_layer in quality_layers
|
||||||
isinstance(quality_layer, (int, float))
|
|
||||||
for quality_layer in quality_layers
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
msg = "quality_layers must be a sequence of numbers"
|
msg = "quality_layers must be a sequence of numbers"
|
||||||
|
|
|
@ -397,7 +397,7 @@ class JpegImageFile(ImageFile.ImageFile):
|
||||||
# self.__offset = self.fp.tell()
|
# self.__offset = self.fp.tell()
|
||||||
break
|
break
|
||||||
s = self.fp.read(1)
|
s = self.fp.read(1)
|
||||||
elif i == 0 or i == 0xFFFF:
|
elif i in {0, 0xFFFF}:
|
||||||
# padded marker or junk; move on
|
# padded marker or junk; move on
|
||||||
s = b"\xff"
|
s = b"\xff"
|
||||||
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
|
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
|
||||||
|
@ -787,6 +787,8 @@ def _save(im, fp, filename):
|
||||||
dpi[0],
|
dpi[0],
|
||||||
dpi[1],
|
dpi[1],
|
||||||
subsampling,
|
subsampling,
|
||||||
|
info.get("restart_marker_blocks", 0),
|
||||||
|
info.get("restart_marker_rows", 0),
|
||||||
qtables,
|
qtables,
|
||||||
comment,
|
comment,
|
||||||
extra,
|
extra,
|
||||||
|
|
|
@ -66,6 +66,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
||||||
self._n_frames = len(self.images)
|
self._n_frames = len(self.images)
|
||||||
self.is_animated = self._n_frames > 1
|
self.is_animated = self._n_frames > 1
|
||||||
|
|
||||||
|
self.__fp = self.fp
|
||||||
self.seek(0)
|
self.seek(0)
|
||||||
|
|
||||||
def seek(self, frame):
|
def seek(self, frame):
|
||||||
|
@ -87,10 +88,12 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
|
||||||
return self.frame
|
return self.frame
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
self.__fp.close()
|
||||||
self.ole.close()
|
self.ole.close()
|
||||||
super().close()
|
super().close()
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
|
self.__fp.close()
|
||||||
self.ole.close()
|
self.ole.close()
|
||||||
super().__exit__()
|
super().__exit__()
|
||||||
|
|
||||||
|
|
|
@ -33,9 +33,6 @@ from . import (
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
from ._binary import o32le
|
from ._binary import o32le
|
||||||
|
|
||||||
# def _accept(prefix):
|
|
||||||
# return JpegImagePlugin._accept(prefix)
|
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
JpegImagePlugin._save(im, fp, filename)
|
JpegImagePlugin._save(im, fp, filename)
|
||||||
|
|
|
@ -82,7 +82,7 @@ class IndirectReference(
|
||||||
collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])
|
collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])
|
||||||
):
|
):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s %s R" % self
|
return f"{self.object_id} {self.generation} R"
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
return self.__str__().encode("us-ascii")
|
return self.__str__().encode("us-ascii")
|
||||||
|
@ -103,7 +103,7 @@ class IndirectReference(
|
||||||
|
|
||||||
class IndirectObjectDef(IndirectReference):
|
class IndirectObjectDef(IndirectReference):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s %s obj" % self
|
return f"{self.object_id} {self.generation} obj"
|
||||||
|
|
||||||
|
|
||||||
class XrefTable:
|
class XrefTable:
|
||||||
|
|
|
@ -1156,6 +1156,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
|
||||||
encoderinfo["duration"] = duration
|
encoderinfo["duration"] = duration
|
||||||
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
|
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
|
||||||
|
|
||||||
|
if len(im_frames) == 1 and not default_image:
|
||||||
|
return im_frames[0]["im"]
|
||||||
|
|
||||||
# animation control
|
# animation control
|
||||||
chunk(
|
chunk(
|
||||||
fp,
|
fp,
|
||||||
|
@ -1391,8 +1394,10 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
|
||||||
chunk(fp, b"eXIf", exif)
|
chunk(fp, b"eXIf", exif)
|
||||||
|
|
||||||
if save_all:
|
if save_all:
|
||||||
_write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
|
im = _write_multiple_frames(
|
||||||
else:
|
im, fp, chunk, rawmode, default_image, append_images
|
||||||
|
)
|
||||||
|
if im:
|
||||||
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
|
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
|
||||||
|
|
||||||
if info:
|
if info:
|
||||||
|
|
|
@ -328,9 +328,6 @@ def _save(im, fp, filename):
|
||||||
fp.write(b"65535\n")
|
fp.write(b"65535\n")
|
||||||
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
|
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
|
||||||
|
|
||||||
# ALTERNATIVE: save via builtin debug function
|
|
||||||
# im._dump(filename)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -244,7 +244,7 @@ class _PyAccessI16_L(PyAccess):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
color = min(color[0], 65535)
|
color = min(color[0], 65535)
|
||||||
|
|
||||||
pixel.l = color & 0xFF # noqa: E741
|
pixel.l = color & 0xFF
|
||||||
pixel.r = color >> 8
|
pixel.r = color >> 8
|
||||||
|
|
||||||
|
|
||||||
|
@ -265,7 +265,7 @@ class _PyAccessI16_B(PyAccess):
|
||||||
except Exception:
|
except Exception:
|
||||||
color = min(color[0], 65535)
|
color = min(color[0], 65535)
|
||||||
|
|
||||||
pixel.l = color >> 8 # noqa: E741
|
pixel.l = color >> 8
|
||||||
pixel.r = color & 0xFF
|
pixel.r = color & 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,7 @@ class SgiImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename):
|
def _save(im, fp, filename):
|
||||||
if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L":
|
if im.mode not in {"RGB", "RGBA", "L"}:
|
||||||
msg = "Unsupported SGI image mode"
|
msg = "Unsupported SGI image mode"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ def _save(im, fp, filename):
|
||||||
# Z Dimension: Number of channels
|
# Z Dimension: Number of channels
|
||||||
z = len(im.mode)
|
z = len(im.mode)
|
||||||
|
|
||||||
if dim == 1 or dim == 2:
|
if dim in {1, 2}:
|
||||||
z = 1
|
z = 1
|
||||||
|
|
||||||
# assert we've got the right number of bands.
|
# assert we've got the right number of bands.
|
||||||
|
|
|
@ -1885,13 +1885,14 @@ class AppendingTiffWriter:
|
||||||
8, # long8
|
8, # long8
|
||||||
]
|
]
|
||||||
|
|
||||||
# StripOffsets = 273
|
Tags = {
|
||||||
# FreeOffsets = 288
|
273, # StripOffsets
|
||||||
# TileOffsets = 324
|
288, # FreeOffsets
|
||||||
# JPEGQTables = 519
|
324, # TileOffsets
|
||||||
# JPEGDCTables = 520
|
519, # JPEGQTables
|
||||||
# JPEGACTables = 521
|
520, # JPEGDCTables
|
||||||
Tags = {273, 288, 324, 519, 520, 521}
|
521, # JPEGACTables
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, fn, new=False):
|
def __init__(self, fn, new=False):
|
||||||
if hasattr(fn, "read"):
|
if hasattr(fn, "read"):
|
||||||
|
@ -1941,8 +1942,6 @@ class AppendingTiffWriter:
|
||||||
|
|
||||||
iimm = self.f.read(4)
|
iimm = self.f.read(4)
|
||||||
if not iimm:
|
if not iimm:
|
||||||
# msg = "nothing written into new page"
|
|
||||||
# raise RuntimeError(msg)
|
|
||||||
# Make it easy to finish a frame without committing to a new one.
|
# Make it easy to finish a frame without committing to a new one.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ def lookup(tag, group=None):
|
||||||
##
|
##
|
||||||
# Map tag numbers to tag info.
|
# Map tag numbers to tag info.
|
||||||
#
|
#
|
||||||
# id: (Name, Type, Length, enum_values)
|
# id: (Name, Type, Length[, enum_values])
|
||||||
#
|
#
|
||||||
# The length here differs from the length in the tiff spec. For
|
# The length here differs from the length in the tiff spec. For
|
||||||
# numbers, the tiff spec is for the number of fields returned. We
|
# numbers, the tiff spec is for the number of fields returned. We
|
||||||
|
@ -427,7 +427,7 @@ def _populate():
|
||||||
|
|
||||||
TAGS_V2[k] = TagInfo(k, *v)
|
TAGS_V2[k] = TagInfo(k, *v)
|
||||||
|
|
||||||
for group, tags in TAGS_V2_GROUPS.items():
|
for tags in TAGS_V2_GROUPS.values():
|
||||||
for k, v in tags.items():
|
for k, v in tags.items():
|
||||||
tags[k] = TagInfo(k, *v)
|
tags[k] = TagInfo(k, *v)
|
||||||
|
|
||||||
|
@ -438,22 +438,6 @@ _populate()
|
||||||
|
|
||||||
TYPES = {}
|
TYPES = {}
|
||||||
|
|
||||||
# was:
|
|
||||||
# TYPES = {
|
|
||||||
# 1: "byte",
|
|
||||||
# 2: "ascii",
|
|
||||||
# 3: "short",
|
|
||||||
# 4: "long",
|
|
||||||
# 5: "rational",
|
|
||||||
# 6: "signed byte",
|
|
||||||
# 7: "undefined",
|
|
||||||
# 8: "signed short",
|
|
||||||
# 9: "signed long",
|
|
||||||
# 10: "signed rational",
|
|
||||||
# 11: "float",
|
|
||||||
# 12: "double",
|
|
||||||
# }
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# These tags are handled by default in libtiff, without
|
# These tags are handled by default in libtiff, without
|
||||||
# adding to the custom dictionary. From tif_dir.c, searching for
|
# adding to the custom dictionary. From tif_dir.c, searching for
|
||||||
|
|
|
@ -885,7 +885,9 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
PyMem_Del(glyph_info);
|
PyMem_Del(glyph_info);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id"));
|
PyObject *imageId = PyObject_GetAttrString(image, "id");
|
||||||
|
id = PyLong_AsSsize_t(imageId);
|
||||||
|
Py_XDECREF(imageId);
|
||||||
im = (Imaging)id;
|
im = (Imaging)id;
|
||||||
|
|
||||||
x_offset -= stroke_width;
|
x_offset -= stroke_width;
|
||||||
|
|
|
@ -1045,6 +1045,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
||||||
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
|
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
|
||||||
Py_ssize_t xdpi = 0, ydpi = 0;
|
Py_ssize_t xdpi = 0, ydpi = 0;
|
||||||
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
|
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
|
||||||
|
Py_ssize_t restart_marker_blocks = 0;
|
||||||
|
Py_ssize_t restart_marker_rows = 0;
|
||||||
PyObject *qtables = NULL;
|
PyObject *qtables = NULL;
|
||||||
unsigned int *qarrays = NULL;
|
unsigned int *qarrays = NULL;
|
||||||
int qtablesLen = 0;
|
int qtablesLen = 0;
|
||||||
|
@ -1057,7 +1059,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(
|
if (!PyArg_ParseTuple(
|
||||||
args,
|
args,
|
||||||
"ss|nnnnnnnnOz#y#y#",
|
"ss|nnnnnnnnnnOz#y#y#",
|
||||||
&mode,
|
&mode,
|
||||||
&rawmode,
|
&rawmode,
|
||||||
&quality,
|
&quality,
|
||||||
|
@ -1068,6 +1070,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
||||||
&xdpi,
|
&xdpi,
|
||||||
&ydpi,
|
&ydpi,
|
||||||
&subsampling,
|
&subsampling,
|
||||||
|
&restart_marker_blocks,
|
||||||
|
&restart_marker_rows,
|
||||||
&qtables,
|
&qtables,
|
||||||
&comment,
|
&comment,
|
||||||
&comment_size,
|
&comment_size,
|
||||||
|
@ -1156,6 +1160,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
|
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
|
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
|
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
|
||||||
|
((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = restart_marker_blocks;
|
||||||
|
((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = restart_marker_rows;
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
|
((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
|
((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
|
||||||
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
|
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
|
||||||
|
|
|
@ -83,6 +83,10 @@ typedef struct {
|
||||||
/* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */
|
/* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */
|
||||||
int subsampling;
|
int subsampling;
|
||||||
|
|
||||||
|
/* Restart marker interval, in MCU blocks or MCU rows, or 0 for none */
|
||||||
|
unsigned int restart_marker_blocks;
|
||||||
|
unsigned int restart_marker_rows;
|
||||||
|
|
||||||
/* Converter input mode (input to the shuffler) */
|
/* Converter input mode (input to the shuffler) */
|
||||||
char rawmode[8 + 1];
|
char rawmode[8 + 1];
|
||||||
|
|
||||||
|
|
|
@ -210,6 +210,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
|
||||||
}
|
}
|
||||||
context->cinfo.smoothing_factor = context->smooth;
|
context->cinfo.smoothing_factor = context->smooth;
|
||||||
context->cinfo.optimize_coding = (boolean)context->optimize;
|
context->cinfo.optimize_coding = (boolean)context->optimize;
|
||||||
|
context->cinfo.restart_interval = context->restart_marker_blocks;
|
||||||
|
context->cinfo.restart_in_rows = context->restart_marker_rows;
|
||||||
if (context->xdpi > 0 && context->ydpi > 0) {
|
if (context->xdpi > 0 && context->ydpi > 0) {
|
||||||
context->cinfo.write_JFIF_header = TRUE;
|
context->cinfo.write_JFIF_header = TRUE;
|
||||||
context->cinfo.density_unit = 1; /* dots per inch */
|
context->cinfo.density_unit = 1; /* dots per inch */
|
||||||
|
@ -218,9 +220,9 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
|
||||||
}
|
}
|
||||||
switch (context->streamtype) {
|
switch (context->streamtype) {
|
||||||
case 1:
|
case 1:
|
||||||
/* tables only -- not yet implemented */
|
/* tables only */
|
||||||
state->errcode = IMAGING_CODEC_CONFIG;
|
jpeg_write_tables(&context->cinfo);
|
||||||
return -1;
|
goto cleanup;
|
||||||
case 2:
|
case 2:
|
||||||
/* image only */
|
/* image only */
|
||||||
jpeg_suppress_tables(&context->cinfo, TRUE);
|
jpeg_suppress_tables(&context->cinfo, TRUE);
|
||||||
|
@ -316,6 +318,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
|
||||||
}
|
}
|
||||||
jpeg_finish_compress(&context->cinfo);
|
jpeg_finish_compress(&context->cinfo);
|
||||||
|
|
||||||
|
cleanup:
|
||||||
/* Clean up */
|
/* Clean up */
|
||||||
if (context->comment) {
|
if (context->comment) {
|
||||||
free(context->comment);
|
free(context->comment);
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
|
|
||||||
#ifdef HAVE_LIBTIFF
|
#ifdef HAVE_LIBTIFF
|
||||||
|
|
||||||
|
#ifdef HAVE_UNISTD_H
|
||||||
|
#include <unistd.h> /* lseek */
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef uint
|
#ifndef uint
|
||||||
#define uint uint32
|
#define uint uint32
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -13,12 +13,6 @@
|
||||||
#include <tiff.h>
|
#include <tiff.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* UNDONE -- what are we using from this? */
|
|
||||||
/*#ifndef _UNISTD_H
|
|
||||||
# include <unistd.h>
|
|
||||||
# endif
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef min
|
#ifndef min
|
||||||
#define min(x, y) ((x > y) ? y : x)
|
#define min(x, y) ((x > y) ? y : x)
|
||||||
#define max(x, y) ((x < y) ? y : x)
|
#define max(x, y) ((x < y) ? y : x)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user