Merge branch 'main' into gha-windows-32-bit

This commit is contained in:
Hugo van Kemenade 2025-01-16 17:01:59 +02:00 committed by GitHub
commit ade99aaa90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 1091 additions and 740 deletions

View File

@ -2,8 +2,4 @@
# gather the coverage data # gather the coverage data
python3 -m pip install coverage python3 -m pip install coverage
if [[ $MATRIX_DOCKER ]]; then python3 -m coverage xml
python3 -m coverage xml --ignore-errors
else
python3 -m coverage xml
fi

View File

@ -3,8 +3,5 @@
set -e set -e
python3 -m coverage erase python3 -m coverage erase
if [ $(uname) == "Darwin" ]; then
export CPPFLAGS="-I/usr/local/miniconda/include";
fi
make clean make clean
make install-coverage make install-coverage

View File

@ -21,7 +21,7 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev sway wl-clipboard libopenblas-dev
fi fi

View File

@ -1 +1 @@
cibuildwheel==2.21.3 cibuildwheel==2.22.0

View File

@ -1,4 +1,4 @@
mypy==1.13.0 mypy==1.14.1
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6 IceSpringPySideStubs-PySide6
ipython ipython

View File

@ -19,7 +19,6 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Follow PEP 8. - Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests.
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged.
## Reporting Issues ## Reporting Issues

View File

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

View File

@ -8,8 +8,8 @@ fi
brew install \ brew install \
freetype \ freetype \
ghostscript \ ghostscript \
jpeg-turbo \
libimagequant \ libimagequant \
libjpeg \
libtiff \ libtiff \
little-cms2 \ little-cms2 \
openjpeg \ openjpeg \

View File

@ -52,7 +52,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v4 uses: cygwin/cygwin-install-action@v5
with: with:
packages: > packages: >
gcc-g++ gcc-g++
@ -133,11 +133,12 @@ jobs:
- name: After success - name: After success
run: | run: |
bash.exe .ci/after_success.sh bash.exe .ci/after_success.sh
rm C:\cygwin\bin\bash.EXE
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
file: ./coverage.xml files: ./coverage.xml
flags: GHA_Cygwin flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }} name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }} token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -44,6 +44,7 @@ jobs:
amazon-2023-amd64, amazon-2023-amd64,
arch, arch,
centos-stream-9-amd64, centos-stream-9-amd64,
centos-stream-10-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-40-amd64, fedora-40-amd64,
@ -89,18 +90,18 @@ jobs:
- name: After success - name: After success
run: | run: |
PATH="$PATH:~/.local/bin"
docker start pillow_container docker start pillow_container
sudo docker cp pillow_container:/Pillow /Pillow
sudo chown -R runner /Pillow
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
docker stop pillow_container docker stop pillow_container
sudo mkdir -p $pil_path sudo mkdir -p $pil_path
sudo cp src/PIL/*.py $pil_path sudo cp src/PIL/*.py $pil_path
cd /Pillow
.ci/after_success.sh .ci/after_success.sh
env:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}

View File

@ -66,18 +66,18 @@ jobs:
mingw-w64-x86_64-libtiff \ mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \ mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \ mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-numpy \ mingw-w64-x86_64-python-numpy \
mingw-w64-x86_64-python3-olefile \ mingw-w64-x86_64-python-olefile \
mingw-w64-x86_64-python3-setuptools \ mingw-w64-x86_64-python-pip \
mingw-w64-x86_64-python-pytest \
mingw-w64-x86_64-python-pytest-cov \
mingw-w64-x86_64-python-pytest-timeout \
mingw-w64-x86_64-python-pyqt6 mingw-w64-x86_64-python-pyqt6
python3 -m ensurepip
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd
- name: Build Pillow - name: Build Pillow
run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . run: CFLAGS="-coverage" python3 -m pip install .
- name: Test Pillow - name: Test Pillow
run: | run: |
@ -85,9 +85,9 @@ jobs:
.ci/test.sh .ci/test.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
file: ./coverage.xml files: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: "MSYS2 MinGW" name: "MSYS2 MinGW"
token: ${{ secrets.CODECOV_ORG_TOKEN }} token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -35,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13"] python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"] architecture: ["x64"]
os: ["windows-latest"] os: ["windows-latest"]
include: include:
@ -221,9 +221,9 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
file: ./coverage.xml files: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }} name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }} token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -42,6 +42,8 @@ jobs:
] ]
python-version: [ python-version: [
"pypy3.10", "pypy3.10",
"3.14",
"3.13t",
"3.13", "3.13",
"3.12", "3.12",
"3.11", "3.11",
@ -52,14 +54,14 @@ jobs:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 } - { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded # Free-threaded
- { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true } - { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+ # M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" } - { os: "macos-13", python-version: "3.9" }
exclude: exclude:
- { os: "macos-latest", python-version: "3.9" } - { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -67,8 +69,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: Quansight-Labs/setup-python@v5
if: "${{ !matrix.disable-gil }}"
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
@ -77,13 +78,6 @@ jobs:
".ci/*.sh" ".ci/*.sh"
"pyproject.toml" "pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.2.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
nogil: ${{ matrix.disable-gil }}
- name: Set PYTHON_GIL - name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}" if: "${{ matrix.disable-gil }}"
run: | run: |
@ -116,7 +110,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher - name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json" run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build - name: Build
@ -156,7 +150,7 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
with: with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}

View File

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

View File

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

View File

@ -13,6 +13,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py" - "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
@ -23,6 +24,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py" - "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
@ -85,7 +87,7 @@ jobs:
CIBW_ARCHS: "aarch64" CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up. # Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_PRERELEASE_PYTHONS: True CIBW_ENABLE: cpython-prerelease
# Extra options for manylinux. # Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
@ -150,10 +152,9 @@ jobs:
env: env:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }} CIBW_BUILD: ${{ matrix.build }}
CIBW_FREE_THREADED_SUPPORT: True CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp39-* CIBW_SKIP: pp39-*
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
@ -228,8 +229,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw" CIBW_CACHE_PATH: "C:\\cibw"
CIBW_FREE_THREADED_SUPPORT: True CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp39-* CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm CIBW_TEST_COMMAND: 'docker run --rm
@ -265,8 +265,6 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip
cache-dependency-path: "Makefile"
- run: make sdist - run: make sdist

5
.gitignore vendored
View File

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

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.2 rev: v0.8.6
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.10 rev: 1.8.0
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.3 rev: v19.1.6
hooks: hooks:
- id: clang-format - id: clang-format
types: [c] types: [c]
@ -50,12 +50,17 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.4 rev: 0.30.0
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
- id: check-renovate - id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.0.0
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0 rev: v1.0.0
hooks: hooks:
@ -67,7 +72,7 @@ repos:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.22 rev: v0.23
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12] additional_dependencies: [trove-classifiers>=2024.10.12]

View File

@ -1,5 +1,8 @@
version: 2 version: 2
sphinx:
configuration: docs/conf.py
formats: [pdf] formats: [pdf]
build: build:

View File

@ -2,20 +2,12 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
11.1.0 (unreleased) 11.1.0 and newer
------------------- ----------------
- Detach PyQt6 QPixmap instance before returning #8509 See GitHub Releases:
[radarhere]
- Corrected EMF DPI #8485 - https://github.com/python-pillow/Pillow/releases
[radarhere]
- Fix IFDRational with a zero denominator #8474
[radarhere]
- Fixed disabling a feature during install #8469
[radarhere]
11.0.0 (2024-10-15) 11.0.0 (2024-10-15)
------------------- -------------------

View File

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

View File

@ -104,7 +104,7 @@ The core image library is designed for fast access to data stored in a few basic
- [Issues](https://github.com/python-pillow/Pillow/issues) - [Issues](https://github.com/python-pillow/Pillow/issues)
- [Pull requests](https://github.com/python-pillow/Pillow/pulls) - [Pull requests](https://github.com/python-pillow/Pillow/pulls)
- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Changelog](https://github.com/python-pillow/Pillow/releases)
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
## Report a Vulnerability ## Report a Vulnerability

View File

@ -12,7 +12,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
* [ ] Create branch and tag for release e.g.: * [ ] Create branch and tag for release e.g.:
```bash ```bash
@ -34,7 +33,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
Released as needed for security, installation or critical bug fixes. Released as needed for security, installation or critical bug fixes.
* [ ] Make necessary changes in `main` branch. * [ ] Make necessary changes in `main` branch.
* [ ] Update `CHANGES.rst`.
* [ ] Check out release branch e.g.: * [ ] Check out release branch e.g.:
```bash ```bash
git checkout -t remotes/origin/5.2.x git checkout -t remotes/origin/5.2.x

View File

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

View File

@ -140,18 +140,11 @@ def assert_image_similar_tofile(
filename: str, filename: str,
epsilon: float, epsilon: float,
msg: str | None = None, msg: str | None = None,
mode: str | None = None,
) -> None: ) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg) assert_image_similar(a, img, epsilon, msg)
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg assert items.count(items[0]) != len(items), msg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

@ -7,7 +7,7 @@ import fuzzers
import packaging import packaging
import pytest import pytest
from PIL import Image, UnidentifiedImageError, features from PIL import Image, features
from Tests.helper import skip_unless_feature from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"): if sys.platform.startswith("win32"):
@ -32,21 +32,17 @@ def test_fuzz_images(path: str) -> None:
fuzzers.fuzz_image(f.read()) fuzzers.fuzz_image(f.read())
assert True assert True
except ( except (
# Known exceptions from Pillow
OSError, OSError,
SyntaxError, SyntaxError,
MemoryError, MemoryError,
ValueError, ValueError,
NotImplementedError, NotImplementedError,
OverflowError, OverflowError,
): # Known Image.* exceptions
# Known exceptions that are through from Pillow
assert True
except (
Image.DecompressionBombError, Image.DecompressionBombError,
Image.DecompressionBombWarning, Image.DecompressionBombWarning,
UnidentifiedImageError,
): ):
# Known Image.* exceptions
assert True assert True
finally: finally:
fuzzers.disable_decompressionbomb_error() fuzzers.disable_decompressionbomb_error()

View File

@ -388,10 +388,12 @@ class TestColorLut3DFilter:
table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16)
lut = ImageFilter.Color3DLUT((5, 6, 7), table) lut = ImageFilter.Color3DLUT((5, 6, 7), table)
assert isinstance(lut.table, numpy.ndarray)
assert lut.table.shape == (table.size,) assert lut.table.shape == (table.size,)
table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16)
lut = ImageFilter.Color3DLUT((5, 6, 7), table) lut = ImageFilter.Color3DLUT((5, 6, 7), table)
assert isinstance(lut.table, numpy.ndarray)
assert lut.table.shape == (table.size,) assert lut.table.shape == (table.size,)
# Check application # Check application

View File

@ -36,9 +36,10 @@ def test_version() -> None:
else: else:
assert function(name) == version assert function(name) == version
if name != "PIL": if name != "PIL":
if name == "zlib" and version is not None: if version is not None:
if name == "zlib" and features.check_feature("zlib_ng"):
version = re.sub(".zlib-ng$", "", version) version = re.sub(".zlib-ng$", "", version)
elif name == "libtiff" and version is not None: elif name == "libtiff":
version = re.sub("t$", "", version) version = re.sub("t$", "", version)
assert version is None or re.search(r"\d+(\.\d+)*$", version) assert version is None or re.search(r"\d+(\.\d+)*$", version)

View File

@ -307,13 +307,8 @@ def test_apng_syntax_errors() -> None:
im.load() im.load()
# we can handle this case gracefully # we can handle this case gracefully
exception = None
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
try:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
except Exception as e:
exception = e
assert exception is None
with pytest.raises(OSError): with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
@ -405,13 +400,8 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
append_images=frames, append_images=frames,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
exception = None
try:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()
except Exception as e:
exception = e
assert exception is None
def test_apng_save_duration_loop(tmp_path: Path) -> None: def test_apng_save_duration_loop(tmp_path: Path) -> None:

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest import pytest
from PIL import Image from PIL import BlpImagePlugin, Image
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -19,6 +19,7 @@ def test_load_blp1() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im: with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
assert im.mode == "RGBA"
im.load() im.load()
@ -37,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(BlpImagePlugin.BLPFormatError):
BlpImagePlugin.BlpImageFile(invalid_file)
def test_save(tmp_path: Path) -> None: def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp") f = str(tmp_path / "temp.blp")

View File

@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None:
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved
BufrStubImagePlugin._handler = None BufrStubImagePlugin.register_handler(None)

View File

@ -4,8 +4,6 @@ import pytest
from PIL import ContainerIO, Image from PIL import ContainerIO, Image
from .helper import hopper
TEST_FILE = "Tests/images/dummy.container" TEST_FILE = "Tests/images/dummy.container"
@ -15,15 +13,15 @@ def test_sanity() -> None:
def test_isatty() -> None: def test_isatty() -> None:
with hopper() as im: with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(im, 0, 0) container = ContainerIO.ContainerIO(fh, 0, 0)
assert container.isatty() is False assert container.isatty() is False
def test_seekable() -> None: def test_seekable() -> None:
with hopper() as im: with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(im, 0, 0) container = ContainerIO.ContainerIO(fh, 0, 0)
assert container.seekable() is True assert container.seekable() is True

View File

@ -4,6 +4,7 @@ import warnings
from collections.abc import Generator from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -1435,7 +1436,8 @@ def test_saving_rgba(tmp_path: Path) -> None:
assert reloaded_rgba.load()[0, 0][3] == 0 assert reloaded_rgba.load()[0, 0][3] == 0
def test_optimizing_p_rgba(tmp_path: Path) -> None: @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))
def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im1 = Image.new("P", (100, 100)) im1 = Image.new("P", (100, 100))
@ -1447,7 +1449,7 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None:
im2 = Image.new("P", (100, 100)) im2 = Image.new("P", (100, 100))
im2.putpalette(data, "RGBA") im2.putpalette(data, "RGBA")
im1.save(out, save_all=True, append_images=[im2]) im1.save(out, save_all=True, append_images=[im2], **params)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.n_frames == 2 assert reloaded.n_frames == 2

View File

@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None:
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved
GribStubImagePlugin._handler = None GribStubImagePlugin.register_handler(None)

View File

@ -85,4 +85,4 @@ def test_handler(tmp_path: Path) -> None:
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved
Hdf5StubImagePlugin._handler = None Hdf5StubImagePlugin.register_handler(None)

View File

@ -253,7 +253,6 @@ def test_truncated_mask() -> None:
try: try:
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected:
assert im.mode == "1" assert im.mode == "1"
# 32 bpp # 32 bpp

View File

@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None:
# Assert # Assert
assert iptc is not None assert iptc is not None
for tag in iptc.keys(): assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found"
if tag[0] == 240:
return
pytest.fail("FotoStation tag not found")
def test_getiptcinfo_zero_padding() -> None: def test_getiptcinfo_zero_padding() -> None:

View File

@ -181,6 +181,10 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200) assert test(100, 200) == (100, 200)
assert test(0) is None # square pixels assert test(0) is None # square pixels
def test_dpi_jfif_cm(self) -> None:
with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
assert im.info["dpi"] == (2.54, 5.08)
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
@ -277,6 +281,9 @@ class TestFileJpeg:
assert not im2.info.get("progressive") assert not im2.info.get("progressive")
assert im3.info.get("progressive") assert im3.info.get("progressive")
if features.check_feature("mozjpeg"):
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im3) assert_image_equal(im1, im3)
assert im1_bytes >= im3_bytes assert im1_bytes >= im3_bytes
@ -349,7 +356,6 @@ class TestFileJpeg:
assert exif.get_ifd(0x8825) == {} assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im) transposed = ImageOps.exif_transpose(im)
assert transposed is not None
exif = transposed.getexif() exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {} assert exif.get_ifd(0x8825) == {}
@ -420,6 +426,10 @@ class TestFileJpeg:
im2 = self.roundtrip(hopper(), progressive=1) im2 = self.roundtrip(hopper(), progressive=1)
im3 = self.roundtrip(hopper(), progression=1) # compatibility im3 = self.roundtrip(hopper(), progression=1) # compatibility
if features.check_feature("mozjpeg"):
assert_image_similar(im1, im2, 9.39)
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
assert_image_equal(im1, im3) assert_image_equal(im1, im3)
assert im2.info.get("progressive") assert im2.info.get("progressive")
@ -1000,8 +1010,13 @@ class TestFileJpeg:
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test" assert reloaded.info["xmp"] == b"XMP test"
im.info["xmp"] = b"1" * 65504 # Check that XMP is not saved from image info
im.save(f) reloaded.save(f)
with Image.open(f) as reloaded:
assert "xmp" not in reloaded.info
im.save(f, xmp=b"1" * 65504)
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504 assert reloaded.info["xmp"] == b"1" * 65504
@ -1022,7 +1037,7 @@ class TestFileJpeg:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.tile = [ im.tile = [
("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
] ]
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
im.load() im.load()

View File

@ -325,6 +325,18 @@ def test_cmyk() -> None:
assert im.getpixel((0, 0)) == (185, 134, 0, 0) assert im.getpixel((0, 0)) == (185, 134, 0, 0)
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@skip_unless_feature_version("jpg_2000", "2.5.3")
def test_cmyk_save() -> None:
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2:
assert jp2.mode == "CMYK"
im = roundtrip(jp2)
assert_image_equal(im, jp2)
@pytest.mark.parametrize("ext", (".j2k", ".jp2")) @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None: def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im: with Image.open("Tests/images/16bit.cropped" + ext) as im:
@ -424,7 +436,8 @@ def test_pclr() -> None:
def test_comment() -> None: def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im: for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"):
with Image.open(path) as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
# Test an image that is truncated partway through a codestream # Test an image that is truncated partway through a codestream
@ -479,8 +492,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None:
out.seek(0) out.seek(0)
while True: while True:
marker = out.read(2) marker = out.read(2)
if not marker: assert marker, "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:

View File

@ -36,11 +36,7 @@ class LibTiffTestCase:
im.load() im.load()
im.getdata() im.getdata()
try:
assert im._compression == "group4" assert im._compression == "group4"
except AttributeError:
print("No _compression")
print(dir(im))
# can we write it back out, in a different form. # can we write it back out, in a different form.
out = str(tmp_path / "temp.png") out = str(tmp_path / "temp.png")
@ -1098,6 +1094,25 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_similar(base_im, im, 0.7) assert_image_similar(base_im, im, 0.7)
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif",
"Tests/images/old-style-jpeg-compression.tif",
],
)
def test_buffering(self, test_file: str) -> None:
# load exif first
with Image.open(open(test_file, "rb", buffering=1048576)) as im:
exif = dict(im.getexif())
# load image before exif
with Image.open(open(test_file, "rb", buffering=1048576)) as im2:
im2.load()
exif_after_load = dict(im2.getexif())
assert exif == exif_after_load
@pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core")
def test_sampleformat_not_corrupted(self) -> None: def test_sampleformat_not_corrupted(self) -> None:
# Assert that a TIFF image with SampleFormat=UINT tag is not corrupted # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted
@ -1127,7 +1142,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.load() im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY # Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9" assert str(e.value) == "decoder error -9"
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:

View File

@ -297,3 +297,15 @@ def test_save_all() -> None:
# Test that a single frame image will not be saved as an MPO # Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True) jpg = roundtrip(im, save_all=True)
assert "mp" not in jpg.info assert "mp" not in jpg.info
def test_save_xmp() -> None:
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im2.encoderinfo = {"xmp": b"Second frame"}
im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2])
assert im_reloaded.info["xmp"] == b"First frame"
im_reloaded.seek(1)
assert im_reloaded.info["xmp"] == b"Second frame"

View File

@ -618,7 +618,7 @@ class TestFilePng:
with Image.open("Tests/images/truncated_image.png") as im: with Image.open("Tests/images/truncated_image.png") as im:
# The file is truncated # The file is truncated
with pytest.raises(OSError): with pytest.raises(OSError):
im.text() im.text
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
assert isinstance(im.text, dict) assert isinstance(im.text, dict)
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
@ -772,22 +772,18 @@ class TestFilePng:
im.seek(1) im.seek(1)
@pytest.mark.parametrize("buffer", (True, False)) @pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer: bool) -> None: def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
old_stdout = sys.stdout
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_PNG_FILE) as im: with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG") im.save(sys.stdout, "PNG")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut): if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:

View File

@ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None:
@pytest.mark.parametrize("buffer", (True, False)) @pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(buffer: bool) -> None: def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
old_stdout = sys.stdout
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM") im.save(sys.stdout, "PPM")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut): if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:

View File

@ -7,7 +7,7 @@ from pathlib import Path
import pytest import pytest
from PIL import Image, ImageSequence, SpiderImagePlugin from PIL import Image, SpiderImagePlugin
from .helper import assert_image_equal, hopper, is_pypy from .helper import assert_image_equal, hopper, is_pypy
@ -153,8 +153,8 @@ def test_nonstack_file() -> None:
def test_nonstack_dos() -> None: def test_nonstack_dos() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
for i, frame in enumerate(ImageSequence.Iterator(im)): with pytest.raises(EOFError):
assert i <= 1, "Non-stack DOS file test failed" im.seek(0)
# for issue #4093 # for issue #4093

View File

@ -115,6 +115,19 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_bigtiff_save(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im = hopper()
im.save(outfile, big_tiff=True)
with Image.open(outfile) as reloaded:
assert reloaded.tag_v2._bigtiff is True
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
with Image.open(outfile) as reloaded:
assert reloaded.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None: def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"): with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif") Image.open("Tests/images/seek_too_large.tif")
@ -733,7 +746,7 @@ class TestFileTiff:
assert reread.n_frames == 3 assert reread.n_frames == 3
def test_fixoffsets(self) -> None: def test_fixoffsets(self) -> None:
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a: with TiffImagePlugin.AppendingTiffWriter(b) as a:
b.seek(0) b.seek(0)
a.fixOffsets(1, isShort=True) a.fixOffsets(1, isShort=True)
@ -746,6 +759,37 @@ class TestFileTiff:
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
a.fixOffsets(1) a.fixOffsets(1)
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**16
b.seek(0)
a.fixOffsets(1, isShort=True)
b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**32
b.seek(0)
a.fixOffsets(1, isShort=True)
b.seek(0)
a.fixOffsets(1, isLong=True)
def test_appending_tiff_writer_writelong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.writeLong(2**32 - 1)
assert b.getvalue() == data + b"\xff\xff\xff\xff"
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.rewriteLastShortToLong(2**32 - 1)
assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff"
def test_saving_icc_profile(self, tmp_path: Path) -> None: def test_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set. # Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs # At the time of writing this will only work for non-compressed tiffs

View File

@ -35,6 +35,13 @@ def test_load() -> None:
assert im.load()[0, 0] == (255, 255, 255) assert im.load()[0, 0] == (255, 255, 255)
def test_load_zero_inch() -> None:
b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10)
with pytest.raises(ValueError):
with Image.open(b):
pass
def test_register_handler(tmp_path: Path) -> None: def test_register_handler(tmp_path: Path) -> None:
class TestHandler(ImageFile.StubHandler): class TestHandler(ImageFile.StubHandler):
methodCalled = False methodCalled = False

View File

@ -189,8 +189,6 @@ class TestImage:
if ext == ".jp2" and not features.check_codec("jpg_2000"): if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available") pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext)) temp_file = str(tmp_path / ("temp." + ext))
if os.path.exists(temp_file):
os.remove(temp_file)
im.save(Path(temp_file)) im.save(Path(temp_file))
def test_fp_name(self, tmp_path: Path) -> None: def test_fp_name(self, tmp_path: Path) -> None:
@ -667,7 +665,7 @@ class TestImage:
# Test illegal image mode # Test illegal image mode
with hopper() as im: with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.remap_palette(None) im.remap_palette([])
def test_remap_palette_transparency(self) -> None: def test_remap_palette_transparency(self) -> None:
im = Image.new("P", (1, 2), (0, 0, 0)) im = Image.new("P", (1, 2), (0, 0, 0))
@ -770,7 +768,7 @@ class TestImage:
assert dict(exif) assert dict(exif)
# Test that exif data is cleared after another load # Test that exif data is cleared after another load
exif.load(None) exif.load(b"")
assert not dict(exif) assert not dict(exif)
# Test loading just the EXIF header # Test loading just the EXIF header
@ -793,6 +791,10 @@ class TestImage:
ifd[36864] = b"0220" ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"} assert exif.get_ifd(0x8769) == {36864: b"0220"}
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"}
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )

View File

@ -271,13 +271,25 @@ class TestImagePutPixelError:
class TestEmbeddable: class TestEmbeddable:
@pytest.mark.xfail(reason="failing test") @pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test")
@pytest.mark.skipif(not is_win32(), reason="requires Windows") @pytest.mark.skipif(not is_win32(), reason="requires Windows")
def test_embeddable(self) -> None: def test_embeddable(self) -> None:
import ctypes import ctypes
from setuptools.command import build_ext from setuptools.command import build_ext
compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
"INCLUDEPY"
).replace("include", "libs")
compiler.add_library_dir(libdir)
try:
compiler.initialize()
except Exception:
pytest.skip("Compiler could not be initialized")
with open("embed_pil.c", "w", encoding="utf-8") as fh: with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\") home = sys.prefix.replace("\\", "\\\\")
fh.write( fh.write(
@ -305,13 +317,6 @@ int main(int argc, char* argv[])
""" """
) )
compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
"INCLUDEPY"
).replace("include", "libs")
compiler.add_library_dir(libdir)
objects = compiler.compile(["embed_pil.c"]) objects = compiler.compile(["embed_pil.c"])
compiler.link_executable(objects, "embed_pil") compiler.link_executable(objects, "embed_pil")

View File

@ -309,7 +309,7 @@ class TestImageResize:
# Test unknown resampling filter # Test unknown resampling filter
with hopper() as im: with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.resize((10, 10), "unknown") im.resize((10, 10), -1)
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_transposed(self) -> None: def test_transposed(self) -> None:

View File

@ -104,20 +104,20 @@ def test_transposed() -> None:
assert im.size == (590, 88) assert im.size == (590, 88)
def test_load_first_unless_jpeg() -> None: def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None:
# Test that thumbnail() still uses draft() for JPEG # Test that thumbnail() still uses draft() for JPEG
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft original_draft = im.draft
def im_draft( def im_draft(
mode: str, size: tuple[int, int] mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None: ) -> tuple[str, tuple[int, int, float, float]] | None:
result = draft(mode, size) result = original_draft(mode, size)
assert result is not None assert result is not None
return result return result
im.draft = im_draft monkeypatch.setattr(im, "draft", im_draft)
im.thumbnail((64, 64)) im.thumbnail((64, 64))

View File

@ -1674,6 +1674,9 @@ def test_continuous_horizontal_edges_polygon() -> None:
def test_discontiguous_corners_polygon() -> None: def test_discontiguous_corners_polygon() -> None:
img, draw = create_base_image_draw((84, 68)) img, draw = create_base_image_draw((84, 68))
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
draw.polygon(
((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK
)
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
draw.polygon( draw.polygon(
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),

View File

@ -93,6 +93,19 @@ class TestImageFile:
assert p.image is not None assert p.image is not None
assert (48, 48) == p.image.size assert (48, 48) == p.image.size
@pytest.mark.filterwarnings("ignore:Corrupt EXIF data")
def test_incremental_tiff(self) -> None:
with ImageFile.Parser() as p:
with open("Tests/images/hopper.tif", "rb") as f:
p.feed(f.read(1024))
# Check that insufficient data was given in the first feed
assert not p.image
p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size
@skip_unless_feature("webp") @skip_unless_feature("webp")
def test_incremental_webp(self) -> None: def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p: with ImageFile.Parser() as p:

View File

@ -405,7 +405,6 @@ def test_exif_transpose() -> None:
else: else:
original_exif = im.info["exif"] original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17) assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im: if orientation_im is base_im:
assert "exif" not in im.info assert "exif" not in im.info
@ -417,7 +416,6 @@ def test_exif_transpose() -> None:
# Repeat the operation to test that it does not keep transposing # Repeat the operation to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im) transposed_im2 = ImageOps.exif_transpose(transposed_im)
assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im) assert_image_equal(transposed_im2, transposed_im)
check(base_im) check(base_im)
@ -433,7 +431,6 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3 assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif() transposed_im._reload_exif()
@ -446,14 +443,12 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3 assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif # Orientation set directly on Image.Exif
im = hopper() im = hopper()
im.getexif()[0x0112] = 3 im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
@ -464,7 +459,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"] del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()

View File

@ -74,6 +74,17 @@ def test_pickle_image(
helper_pickle_file(tmp_path, protocol, test_file, test_mode) helper_pickle_file(tmp_path, protocol, test_file, test_mode)
def test_pickle_jpeg() -> None:
# Arrange
with Image.open("Tests/images/hopper.jpg") as image:
# Act: roundtrip
unpickled_image = pickle.loads(pickle.dumps(image))
# Assert
assert len(unpickled_image.layer) == 3
assert unpickled_image.layers == 3
def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange # Arrange
filename = str(tmp_path / "temp.pkl") filename = str(tmp_path / "temp.pkl")

View File

@ -1,7 +1,7 @@
# Documentation: https://docs.codecov.com/docs/codecov-yaml # Documentation: https://docs.codecov.com/docs/codecov-yaml
codecov: codecov:
# Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" # Avoid "Missing base report" due to committing with "[CI skip]"
# https://github.com/codecov/support/issues/363 # https://github.com/codecov/support/issues/363
# https://docs.codecov.com/docs/comparing-commits # https://docs.codecov.com/docs/comparing-commits
allow_coverage_offsets: true allow_coverage_offsets: true

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install openjpeg # install openjpeg
archive=openjpeg-2.5.2 archive=openjpeg-2.5.3
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install webp # install webp
archive=libwebp-1.4.0 archive=libwebp-1.5.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

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

View File

@ -55,7 +55,7 @@ master_doc = "index"
project = "Pillow (PIL Fork)" project = "Pillow (PIL Fork)"
copyright = ( copyright = (
"1995-2011 Fredrik Lundh and contributors, " "1995-2011 Fredrik Lundh and contributors, "
"2010-2024 Jeffrey A. Clark and contributors." "2010 Jeffrey A. Clark and contributors."
) )
author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)"

View File

@ -175,6 +175,14 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object. ``Image.Image.getim()``, which returns a ``Capsule`` object.
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.1.0
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
Removed features Removed features
---------------- ----------------

View File

@ -572,10 +572,19 @@ JPEG 2000
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to ``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. ``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports .. versionadded:: 8.3.0
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
(``.jp2`` or ``.jpx`` files). subsampled components.
.. versionadded:: 10.4.0
Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later.
.. versionadded:: 11.1.0
Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later.
Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed
JPEG 2000 files (``.jp2`` or ``.jpx`` files).
When loading, if you set the ``mode`` on the image prior to the When loading, if you set the ``mode`` on the image prior to the
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
@ -1199,6 +1208,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.4.0 .. versionadded:: 8.4.0
**big_tiff**
If true, the image will be saved as a BigTIFF.
.. versionadded:: 11.1.0
**compression** **compression**
A string containing the desired compression method for the A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression file. (valid only with libtiff installed) Valid compression

View File

@ -678,7 +678,7 @@ Reading from URL
from PIL import Image from PIL import Image
from urllib.request import urlopen from urllib.request import urlopen
url = "https://python-pillow.org/assets/images/pillow-logo.png" url = "https://python-pillow.github.io/assets/images/pillow-logo.png"
img = Image.open(urlopen(url)) img = Image.open(urlopen(url))

View File

@ -58,7 +58,7 @@ Many of Pillow's features require external libraries:
* **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**,
**2.4.0**, **2.5.0** and **2.5.2**. **2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**.
* Pillow does **not** support the earlier **1.5** series which ships * Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie. with Debian Jessie.
@ -148,13 +148,7 @@ Many of Pillow's features require external libraries:
The easiest way to install external libraries is via `Homebrew The easiest way to install external libraries is via `Homebrew
<https://brew.sh/>`_. After you install Homebrew, run:: <https://brew.sh/>`_. After you install Homebrew, run::
brew install libjpeg libtiff little-cms2 openjpeg webp brew install libjpeg libraqm libtiff little-cms2 openjpeg webp
To install libraqm on macOS use Homebrew to install its dependencies::
brew install freetype harfbuzz fribidi
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
.. tab:: Windows .. tab:: Windows
@ -195,11 +189,6 @@ Many of Pillow's features require external libraries:
mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libraqm mingw-w64-x86_64-libraqm
https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with
MSYS2. To workaround this, before installing Pillow you must run::
export SETUPTOOLS_USE_DISTUTILS=stdlib
.. tab:: FreeBSD .. tab:: FreeBSD
.. Note:: Only FreeBSD 10 and 11 tested .. Note:: Only FreeBSD 10 and 11 tested

View File

@ -27,6 +27,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 | | CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 | | Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 | | Fedora 40 | 3.12 | x86-64 |
@ -53,7 +55,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 | | Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | PyPy3 | | | | PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 | | | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9 (Cygwin) | x86-64 | | | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
@ -73,7 +75,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+ +==================================+============================+==================+==============+
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm | | macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
| +----------------------------+------------------+ | | +----------------------------+------------------+ |
| | 3.8 | 10.4.0 | | | | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+

View File

@ -54,6 +54,7 @@ Feature version numbers are available only where stated.
Support for the following features can be checked: Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available.
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.

View File

@ -0,0 +1,80 @@
11.1.0
------
Deprecations
============
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
API Changes
===========
Writing XMP bytes to JPEG and MPO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow 11.0.0 added writing XMP data to JPEG and MPO images::
im.info["xmp"] = b"test"
im.save("out.jpg")
However, this meant that XMP data was automatically kept from an opened image,
which is inconsistent with the rest of Pillow's behaviour. This functionality
has been removed. To write XMP data, the ``xmp`` argument can still be used for
JPEG files::
im.save("out.jpg", xmp=b"test")
To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now
be used::
second_im.encoderinfo = {"xmp": b"test"}
im.save("out.mpo", save_all=True, append_images=[second_im])
API Additions
=============
Check for zlib-ng
^^^^^^^^^^^^^^^^^
You can check if Pillow has been built against the zlib-ng version of the
zlib library, and what version of zlib-ng is being used::
from PIL import features
features.check_feature("zlib_ng") # True or False
features.version_feature("zlib_ng") # "2.2.2" for example, or None
Saving TIFF as BigTIFF
^^^^^^^^^^^^^^^^^^^^^^
TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
im.save("out.tiff", big_tiff=True)
Other Changes
=============
Reading JPEG 2000 comments
^^^^^^^^^^^^^^^^^^^^^^^^^^
When opening a JPEG 2000 image, the comment may now be read into
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.
Saving JPEG 2000 CMYK images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files.
Minimum C version
^^^^^^^^^^^^^^^^^
C99 is now the minimum version of C required to compile Pillow from source.
zlib-ng in wheels
^^^^^^^^^^^^^^^^^
Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG
was found to be more than twice as fast at higher compression levels.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
11.1.0
11.0.0 11.0.0
10.4.0 10.4.0
10.3.0 10.3.0

View File

@ -73,10 +73,10 @@ optional-dependencies.typing = [
optional-dependencies.xmp = [ optional-dependencies.xmp = [
"defusedxml", "defusedxml",
] ]
urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" urls.Changelog = "https://github.com/python-pillow/Pillow/releases"
urls.Documentation = "https://pillow.readthedocs.io" urls.Documentation = "https://pillow.readthedocs.io"
urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi"
urls.Homepage = "https://python-pillow.org" urls.Homepage = "https://python-pillow.github.io"
urls.Mastodon = "https://fosstodon.org/@pillow" urls.Mastodon = "https://fosstodon.org/@pillow"
urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
urls.Source = "https://github.com/python-pillow/Pillow" urls.Source = "https://github.com/python-pillow/Pillow"
@ -94,10 +94,18 @@ version = { attr = "PIL.__version__" }
[tool.cibuildwheel] [tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh" before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1 build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
# Disable platform guessing on macOS
macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh" test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests" test-extras = "tests"
[tool.cibuildwheel.macos.environment]
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib"
[tool.black] [tool.black]
exclude = "wheels/multibuild" exclude = "wheels/multibuild"

View File

@ -344,7 +344,7 @@ class pil_build_ext(build_ext):
for x in ("raqm", "fribidi") for x in ("raqm", "fribidi")
] ]
+ [ + [
("disable-platform-guessing", None, "Disable platform guessing on Linux"), ("disable-platform-guessing", None, "Disable platform guessing"),
("debug", None, "Debug logging"), ("debug", None, "Debug logging"),
] ]
+ [("add-imaging-libs=", None, "Add libs to _imaging build")] + [("add-imaging-libs=", None, "Add libs to _imaging build")]
@ -393,13 +393,14 @@ class pil_build_ext(build_ext):
self.feature.required.discard(x) self.feature.required.discard(x)
_dbg("Disabling %s", x) _dbg("Disabling %s", x)
if getattr(self, f"enable_{x}"): if getattr(self, f"enable_{x}"):
msg = f"Conflicting options: --enable-{x} and --disable-{x}" msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'"
raise ValueError(msg) raise ValueError(msg)
if x == "freetype": if x == "freetype":
_dbg("--disable-freetype implies --disable-raqm") _dbg("'-C freetype=disable' implies '-C raqm=disable'")
if getattr(self, "enable_raqm"): if getattr(self, "enable_raqm"):
msg = ( msg = (
"Conflicting options: --enable-raqm and --disable-freetype" "Conflicting options: "
"'-C raqm=enable' and '-C freetype=disable'"
) )
raise ValueError(msg) raise ValueError(msg)
setattr(self, "disable_raqm", True) setattr(self, "disable_raqm", True)
@ -407,15 +408,17 @@ class pil_build_ext(build_ext):
_dbg("Requiring %s", x) _dbg("Requiring %s", x)
self.feature.required.add(x) self.feature.required.add(x)
if x == "raqm": if x == "raqm":
_dbg("--enable-raqm implies --enable-freetype") _dbg("'-C raqm=enable' implies '-C freetype=enable'")
self.feature.required.add("freetype") self.feature.required.add("freetype")
for x in ("raqm", "fribidi"): for x in ("raqm", "fribidi"):
if getattr(self, f"vendor_{x}"): if getattr(self, f"vendor_{x}"):
if getattr(self, "disable_raqm"): if getattr(self, "disable_raqm"):
msg = f"Conflicting options: --vendor-{x} and --disable-raqm" msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'"
raise ValueError(msg) raise ValueError(msg)
if x == "fribidi" and not getattr(self, "vendor_raqm"): if x == "fribidi" and not getattr(self, "vendor_raqm"):
msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" msg = (
f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'"
)
raise ValueError(msg) raise ValueError(msg)
_dbg("Using vendored version of %s", x) _dbg("Using vendored version of %s", x)
self.feature.vendor.add(x) self.feature.vendor.add(x)
@ -448,7 +451,7 @@ class pil_build_ext(build_ext):
def get_macos_sdk_path(self) -> str | None: def get_macos_sdk_path(self) -> str | None:
try: try:
sdk_path = ( sdk_path = (
subprocess.check_output(["xcrun", "--show-sdk-path"]) subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"])
.strip() .strip()
.decode("latin1") .decode("latin1")
) )
@ -606,6 +609,7 @@ class pil_build_ext(build_ext):
_add_directory(library_dirs, "/usr/X11/lib") _add_directory(library_dirs, "/usr/X11/lib")
_add_directory(include_dirs, "/usr/X11/include") _add_directory(include_dirs, "/usr/X11/include")
# Add the macOS SDK path.
sdk_path = self.get_macos_sdk_path() sdk_path = self.get_macos_sdk_path()
if sdk_path: if sdk_path:
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
@ -690,6 +694,8 @@ class pil_build_ext(build_ext):
feature.set("zlib", "z") feature.set("zlib", "z")
elif sys.platform == "win32" and _find_library_file(self, "zlib"): elif sys.platform == "win32" and _find_library_file(self, "zlib"):
feature.set("zlib", "zlib") # alternative name feature.set("zlib", "zlib") # alternative name
elif sys.platform == "win32" and _find_library_file(self, "zdll"):
feature.set("zlib", "zdll") # dll import library
if feature.want("jpeg"): if feature.want("jpeg"):
_dbg("Looking for jpeg") _dbg("Looking for jpeg")
@ -1044,7 +1050,7 @@ except DependencyException as err:
msg = f""" msg = f"""
The headers or library files could not be found for {str(err)}, The headers or library files could not be found for {str(err)},
which was requested by the option flag --enable-{str(err)} which was requested by the option flag '-C {str(err)}=enable'
""" """
sys.stderr.write(msg) sys.stderr.write(msg)

View File

@ -259,21 +259,36 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self) -> None: def _open(self) -> None:
self.magic = self.fp.read(4) self.magic = self.fp.read(4)
if not _accept(self.magic):
self.fp.seek(5, os.SEEK_CUR)
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
self.fp.seek(2, os.SEEK_CUR)
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
msg = f"Bad BLP magic {repr(self.magic)}" msg = f"Bad BLP magic {repr(self.magic)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
self._mode = "RGBA" if self._blp_alpha_depth else "RGB" compression = struct.unpack("<i", self.fp.read(4))[0]
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] if self.magic == b"BLP1":
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
else:
encoding = struct.unpack("<b", self.fp.read(1))[0]
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
self.fp.seek(1, os.SEEK_CUR) # mips
self._size = struct.unpack("<II", self.fp.read(8))
args: tuple[int, int, bool] | tuple[int, int, bool, int]
if self.magic == b"BLP1":
encoding = struct.unpack("<i", self.fp.read(4))[0]
self.fp.seek(4, os.SEEK_CUR) # subtype
args = (compression, encoding, alpha)
offset = 28
else:
args = (compression, encoding, alpha, alpha_encoding)
offset = 20
decoder = self.magic.decode()
self._mode = "RGBA" if alpha else "RGB"
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
class _BLPBaseDecoder(ImageFile.PyDecoder): class _BLPBaseDecoder(ImageFile.PyDecoder):
@ -281,7 +296,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try: try:
self._read_blp_header() self._read_header()
self._load() self._load()
except struct.error as e: except struct.error as e:
msg = "Truncated BLP file" msg = "Truncated BLP file"
@ -292,25 +307,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
def _load(self) -> None: def _load(self) -> None:
pass pass
def _read_blp_header(self) -> None: def _read_header(self) -> None:
assert self.fd is not None self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self.fd.seek(4) self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
self.fd.seek(1, os.SEEK_CUR) # mips
self.size = struct.unpack("<II", self._safe_read(8))
if isinstance(self, BLP1Decoder):
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
self.fd.seek(4, os.SEEK_CUR) # subtype
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length: int) -> bytes: def _safe_read(self, length: int) -> bytes:
assert self.fd is not None assert self.fd is not None
@ -326,9 +325,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a)) ret.append((b, g, r, a))
return ret return ret
def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray: def _read_bgra(
self, palette: list[tuple[int, int, int, int]], alpha: bool
) -> bytearray:
data = bytearray() data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0])) _data = BytesIO(self._safe_read(self._lengths[0]))
while True: while True:
try: try:
(offset,) = struct.unpack("<B", _data.read(1)) (offset,) = struct.unpack("<B", _data.read(1))
@ -336,7 +337,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
break break
b, g, r, a = palette[offset] b, g, r, a = palette[offset]
d: tuple[int, ...] = (r, g, b) d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth: if alpha:
d += (a,) d += (a,)
data.extend(d) data.extend(d)
return data return data
@ -344,19 +345,21 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder): class BLP1Decoder(_BLPBaseDecoder):
def _load(self) -> None: def _load(self) -> None:
if self._blp_compression == Format.JPEG: self._compression, self._encoding, alpha = self.args
if self._compression == Format.JPEG:
self._decode_jpeg_stream() self._decode_jpeg_stream()
elif self._blp_compression == 1: elif self._compression == 1:
if self._blp_encoding in (4, 5): if self._encoding in (4, 5):
palette = self._read_palette() palette = self._read_palette()
data = self._read_bgra(palette) data = self._read_bgra(palette, alpha)
self.set_as_raw(data) self.set_as_raw(data)
else: else:
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" msg = f"Unsupported BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
else: else:
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
def _decode_jpeg_stream(self) -> None: def _decode_jpeg_stream(self) -> None:
@ -365,65 +368,61 @@ class BLP1Decoder(_BLPBaseDecoder):
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4)) (jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size) jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None assert self.fd is not None
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this? self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0]) data = self._safe_read(self._lengths[0])
data = jpeg_header + data data = jpeg_header + data
image = JpegImageFile(BytesIO(data)) image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size) Image._decompression_bomb_check(image.size)
if image.mode == "CMYK": if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0] args = image.tile[0].args
assert isinstance(args, tuple) assert isinstance(args, tuple)
image.tile = [ image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK")) self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
]
r, g, b = image.convert("RGB").split()
reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes())
class BLP2Decoder(_BLPBaseDecoder): class BLP2Decoder(_BLPBaseDecoder):
def _load(self) -> None: def _load(self) -> None:
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
palette = self._read_palette() palette = self._read_palette()
assert self.fd is not None assert self.fd is not None
self.fd.seek(self._blp_offsets[0]) self.fd.seek(self._offsets[0])
if self._blp_compression == 1: if self._compression == 1:
# Uncompressed or DirectX compression # Uncompressed or DirectX compression
if self._blp_encoding == Encoding.UNCOMPRESSED: if self._encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette) data = self._read_bgra(palette, alpha)
elif self._blp_encoding == Encoding.DXT: elif self._encoding == Encoding.DXT:
data = bytearray() data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1: if self._alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8 linesize = (self.state.xsize + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4): for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt1( for d in decode_dxt1(self._safe_read(linesize), alpha):
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
):
data += d data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT3: elif self._alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.size[0] + 3) // 4 * 16 linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4): for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)): for d in decode_dxt3(self._safe_read(linesize)):
data += d data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT5: elif self._alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.size[0] + 3) // 4 * 16 linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4): for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)): for d in decode_dxt5(self._safe_read(linesize)):
data += d data += d
else: else:
msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
else: else:
msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" msg = f"Unknown BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
else: else:
msg = f"Unknown BLP compression {repr(self._blp_compression)}" msg = f"Unknown BLP compression {repr(self._compression)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
self.set_as_raw(data) self.set_as_raw(data)
@ -472,8 +471,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
assert im.palette is not None assert im.palette is not None
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
if magic == b"BLP1":
fp.write(struct.pack("<L", alpha_depth))
else:
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED)) fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0)) fp.write(struct.pack("<b", alpha_depth))
fp.write(struct.pack("<b", 0)) # alpha encoding fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size)) fp.write(struct.pack("<II", *im.size))

View File

@ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
) )
ImageFile._save( ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:

View File

@ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)]) ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)])
fp.write(b"\n%%%%EndBinary\n") fp.write(b"\n%%%%EndBinary\n")
fp.write(b"grestore end\n") fp.write(b"grestore end\n")

View File

@ -303,38 +303,38 @@ TAGS = {
class GPS(IntEnum): class GPS(IntEnum):
GPSVersionID = 0 GPSVersionID = 0x00
GPSLatitudeRef = 1 GPSLatitudeRef = 0x01
GPSLatitude = 2 GPSLatitude = 0x02
GPSLongitudeRef = 3 GPSLongitudeRef = 0x03
GPSLongitude = 4 GPSLongitude = 0x04
GPSAltitudeRef = 5 GPSAltitudeRef = 0x05
GPSAltitude = 6 GPSAltitude = 0x06
GPSTimeStamp = 7 GPSTimeStamp = 0x07
GPSSatellites = 8 GPSSatellites = 0x08
GPSStatus = 9 GPSStatus = 0x09
GPSMeasureMode = 10 GPSMeasureMode = 0x0A
GPSDOP = 11 GPSDOP = 0x0B
GPSSpeedRef = 12 GPSSpeedRef = 0x0C
GPSSpeed = 13 GPSSpeed = 0x0D
GPSTrackRef = 14 GPSTrackRef = 0x0E
GPSTrack = 15 GPSTrack = 0x0F
GPSImgDirectionRef = 16 GPSImgDirectionRef = 0x10
GPSImgDirection = 17 GPSImgDirection = 0x11
GPSMapDatum = 18 GPSMapDatum = 0x12
GPSDestLatitudeRef = 19 GPSDestLatitudeRef = 0x13
GPSDestLatitude = 20 GPSDestLatitude = 0x14
GPSDestLongitudeRef = 21 GPSDestLongitudeRef = 0x15
GPSDestLongitude = 22 GPSDestLongitude = 0x16
GPSDestBearingRef = 23 GPSDestBearingRef = 0x17
GPSDestBearing = 24 GPSDestBearing = 0x18
GPSDestDistanceRef = 25 GPSDestDistanceRef = 0x19
GPSDestDistance = 26 GPSDestDistance = 0x1A
GPSProcessingMethod = 27 GPSProcessingMethod = 0x1B
GPSAreaInformation = 28 GPSAreaInformation = 0x1C
GPSDateStamp = 29 GPSDateStamp = 0x1D
GPSDifferential = 30 GPSDifferential = 0x1E
GPSHPositioningError = 31 GPSHPositioningError = 0x1F
"""Maps EXIF GPS tags to tag names.""" """Maps EXIF GPS tags to tag names."""
@ -342,40 +342,41 @@ GPSTAGS = {i.value: i.name for i in GPS}
class Interop(IntEnum): class Interop(IntEnum):
InteropIndex = 1 InteropIndex = 0x0001
InteropVersion = 2 InteropVersion = 0x0002
RelatedImageFileFormat = 4096 RelatedImageFileFormat = 0x1000
RelatedImageWidth = 4097 RelatedImageWidth = 0x1001
RelatedImageHeight = 4098 RelatedImageHeight = 0x1002
class IFD(IntEnum): class IFD(IntEnum):
Exif = 34665 Exif = 0x8769
GPSInfo = 34853 GPSInfo = 0x8825
Makernote = 37500 MakerNote = 0x927C
Interop = 40965 Makernote = 0x927C # Deprecated
Interop = 0xA005
IFD1 = -1 IFD1 = -1
class LightSource(IntEnum): class LightSource(IntEnum):
Unknown = 0 Unknown = 0x00
Daylight = 1 Daylight = 0x01
Fluorescent = 2 Fluorescent = 0x02
Tungsten = 3 Tungsten = 0x03
Flash = 4 Flash = 0x04
Fine = 9 Fine = 0x09
Cloudy = 10 Cloudy = 0x0A
Shade = 11 Shade = 0x0B
DaylightFluorescent = 12 DaylightFluorescent = 0x0C
DayWhiteFluorescent = 13 DayWhiteFluorescent = 0x0D
CoolWhiteFluorescent = 14 CoolWhiteFluorescent = 0x0E
WhiteFluorescent = 15 WhiteFluorescent = 0x0F
StandardLightA = 17 StandardLightA = 0x11
StandardLightB = 18 StandardLightB = 0x12
StandardLightC = 19 StandardLightC = 0x13
D55 = 20 D55 = 0x14
D65 = 21 D65 = 0x15
D75 = 22 D75 = 0x16
D50 = 23 D50 = 0x17
ISO = 24 ISO = 0x18
Other = 255 Other = 0xFF

View File

@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile):
framesize = i32(s) framesize = i32(s)
self.decodermaxblock = framesize self.decodermaxblock = framesize
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)] self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)]
self.__offset += framesize self.__offset += framesize

View File

@ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile):
"raw", "raw",
(x, y, x1, y1), (x, y, x1, y1),
i32(s, i) + 28, i32(s, i) + 28,
(self.rawmode,), self.rawmode,
) )
) )

View File

@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile):
self._mode = "RGBA" self._mode = "RGBA"
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
elif format == Format.UNCOMPRESSED: elif format == Format.UNCOMPRESSED:
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")]
else: else:
msg = f"Invalid texture compression format: {repr(format)}" msg = f"Invalid texture compression format: {repr(format)}"
raise ValueError(msg) raise ValueError(msg)

View File

@ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile):
"raw", "raw",
(0, 0) + self.size, (0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4, 7 + true_color_offset + 4 + 256 * 4,
("L", 0, 1), "L",
) )
] ]

View File

@ -695,8 +695,9 @@ def _write_multiple_frames(
) )
background = _get_background(im_frame, color) background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background) background_im = Image.new("P", im_frame.size, background)
assert im_frames[0].im.palette is not None first_palette = im_frames[0].im.palette
background_im.putpalette(im_frames[0].im.palette) assert first_palette is not None
background_im.putpalette(first_palette, first_palette.mode)
bbox = _getbbox(background_im, im_frame)[1] bbox = _getbbox(background_im, im_frame)[1]
elif encoderinfo.get("optimize") and im_frame.mode != "1": elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo: if "transparency" not in encoderinfo:

View File

@ -357,7 +357,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
name = "".join([name[: 92 - len(ext)], ext]) name = "".join([name[: 92 - len(ext)], ext])
fp.write(f"Name: {name}\r\n".encode("ascii")) fp.write(f"Name: {name}\r\n".encode("ascii"))
fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii"))
fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
if im.mode in ["P", "PA"]: if im.mode in ["P", "PA"]:
fp.write(b"Lut: 1\r\n") fp.write(b"Lut: 1\r\n")

View File

@ -692,13 +692,10 @@ class Image:
) )
def __repr__(self) -> str: def __repr__(self) -> str:
return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( return (
self.__class__.__module__, f"<{self.__class__.__module__}.{self.__class__.__name__} "
self.__class__.__name__, f"image mode={self.mode} size={self.size[0]}x{self.size[1]} "
self.mode, f"at 0x{id(self):X}>"
self.size[0],
self.size[1],
id(self),
) )
def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
@ -707,14 +704,8 @@ class Image:
# Same as __repr__ but without unpredictable id(self), # Same as __repr__ but without unpredictable id(self),
# to keep Jupyter notebook `text/plain` output stable. # to keep Jupyter notebook `text/plain` output stable.
p.text( p.text(
"<%s.%s image mode=%s size=%dx%d>" f"<{self.__class__.__module__}.{self.__class__.__name__} "
% ( f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>"
self.__class__.__module__,
self.__class__.__name__,
self.mode,
self.size[0],
self.size[1],
)
) )
def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None:
@ -763,7 +754,7 @@ class Image:
def __setstate__(self, state: list[Any]) -> None: def __setstate__(self, state: list[Any]) -> None:
Image.__init__(self) Image.__init__(self)
info, mode, size, palette, data = state info, mode, size, palette, data = state[:5]
self.info = info self.info = info
self._mode = mode self._mode = mode
self._size = size self._size = size
@ -1574,7 +1565,7 @@ class Image:
for subifd_offset in subifd_offsets: for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(513): if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
assert exif._info is not None assert exif._info is not None
ifds.append((ifd1, exif._info.next)) ifds.append((ifd1, exif._info.next))
@ -1586,11 +1577,11 @@ class Image:
fp = self.fp fp = self.fp
if ifd is not None: if ifd is not None:
thumbnail_offset = ifd.get(513) thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
if thumbnail_offset is not None: if thumbnail_offset is not None:
thumbnail_offset += getattr(self, "_exif_offset", 0) thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset) self.fp.seek(thumbnail_offset)
data = self.fp.read(ifd.get(514)) data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount))
fp = io.BytesIO(data) fp = io.BytesIO(data)
with open(fp) as im: with open(fp) as im:
@ -2550,7 +2541,7 @@ class Image:
filename: str | bytes = "" filename: str | bytes = ""
open_fp = False open_fp = False
if is_path(fp): if is_path(fp):
filename = os.path.realpath(os.fspath(fp)) filename = os.fspath(fp)
open_fp = True open_fp = True
elif fp == sys.stdout: elif fp == sys.stdout:
try: try:
@ -2559,13 +2550,13 @@ class Image:
pass pass
if not filename and hasattr(fp, "name") and is_path(fp.name): if not filename and hasattr(fp, "name") and is_path(fp.name):
# only set the name for metadata purposes # only set the name for metadata purposes
filename = os.path.realpath(os.fspath(fp.name)) filename = os.fspath(fp.name)
# may mutate self! # may mutate self!
self._ensure_mutable() self._ensure_mutable()
save_all = params.pop("save_all", False) save_all = params.pop("save_all", False)
self.encoderinfo = params self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
self.encoderconfig: tuple[Any, ...] = () self.encoderconfig: tuple[Any, ...] = ()
preinit() preinit()
@ -2612,6 +2603,11 @@ class Image:
except PermissionError: except PermissionError:
pass pass
raise raise
finally:
try:
del self.encoderinfo
except AttributeError:
pass
if open_fp: if open_fp:
fp.close() fp.close()
@ -3463,7 +3459,7 @@ def open(
exclusive_fp = False exclusive_fp = False
filename: str | bytes = "" filename: str | bytes = ""
if is_path(fp): if is_path(fp):
filename = os.path.realpath(os.fspath(fp)) filename = os.fspath(fp)
if filename: if filename:
fp = builtins.open(filename, "rb") fp = builtins.open(filename, "rb")
@ -3893,7 +3889,7 @@ class Exif(_ExifBase):
gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo)
print(gps_ifd) print(gps_ifd)
Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``,
``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``.
:py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data::
@ -4027,6 +4023,9 @@ class Exif(_ExifBase):
head = self._get_head() head = self._get_head()
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, ifd_dict in self._ifds.items():
if tag not in self:
ifd[tag] = ifd_dict
for tag, value in self.items(): for tag, value in self.items():
if tag in [ if tag in [
ExifTags.IFD.Exif, ExifTags.IFD.Exif,
@ -4056,11 +4055,11 @@ class Exif(_ExifBase):
ifd = self._get_ifd_dict(offset, tag) ifd = self._get_ifd_dict(offset, tag)
if ifd is not None: if ifd is not None:
self._ifds[tag] = ifd self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]:
if ExifTags.IFD.Exif not in self._ifds: if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif) self.get_ifd(ExifTags.IFD.Exif)
tag_data = self._ifds[ExifTags.IFD.Exif][tag] tag_data = self._ifds[ExifTags.IFD.Exif][tag]
if tag == ExifTags.IFD.Makernote: if tag == ExifTags.IFD.MakerNote:
from .TiffImagePlugin import ImageFileDirectory_v2 from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data[:8] == b"FUJIFILM": if tag_data[:8] == b"FUJIFILM":
@ -4147,7 +4146,7 @@ class Exif(_ExifBase):
ifd = { ifd = {
k: v k: v
for (k, v) in ifd.items() for (k, v) in ifd.items()
if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote)
} }
return ifd return ifd

View File

@ -98,8 +98,8 @@ def _tilesort(t: _Tile) -> int:
class _Tile(NamedTuple): class _Tile(NamedTuple):
codec_name: str codec_name: str
extents: tuple[int, int, int, int] | None extents: tuple[int, int, int, int] | None
offset: int offset: int = 0
args: tuple[Any, ...] | str | None args: tuple[Any, ...] | str | None = None
# #
@ -120,7 +120,7 @@ class ImageFile(Image.Image):
self.custom_mimetype: str | None = None self.custom_mimetype: str | None = None
self.tile: list[_Tile] = [] self.tile: list[_Tile] = []
""" A list of tile descriptors, or ``None`` """ """ A list of tile descriptors """
self.readonly = 1 # until we know better self.readonly = 1 # until we know better
@ -130,7 +130,7 @@ class ImageFile(Image.Image):
if is_path(fp): if is_path(fp):
# filename # filename
self.fp = open(fp, "rb") self.fp = open(fp, "rb")
self.filename = os.path.realpath(os.fspath(fp)) self.filename = os.fspath(fp)
self._exclusive_fp = True self._exclusive_fp = True
else: else:
# stream # stream

View File

@ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter):
ch_out = channels or ch_in ch_out = channels or ch_in
size_1d, size_2d, size_3d = self.size size_1d, size_2d, size_3d = self.size
table = [0] * (size_1d * size_2d * size_3d * ch_out) table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out)
idx_in = 0 idx_in = 0
idx_out = 0 idx_out = 0
for b in range(size_3d): for b in range(size_3d):

View File

@ -270,7 +270,7 @@ class FreeTypeFont:
) )
if is_path(font): if is_path(font):
font = os.path.realpath(os.fspath(font)) font = os.fspath(font)
if sys.platform == "win32": if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode() font_bytes_path = font if isinstance(font, bytes) else font.encode()
try: try:

View File

@ -104,28 +104,17 @@ def grab(
def grabclipboard() -> Image.Image | list[str] | None: def grabclipboard() -> Image.Image | list[str] | None:
if sys.platform == "darwin": if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png") p = subprocess.run(
os.close(fh) ["osascript", "-e", "get the clipboard as «class PNGf»"],
commands = [ capture_output=True,
'set theFile to (open for access POSIX file "' )
+ filepath if p.returncode != 0:
+ '" with write permission)', return None
"try",
" write (the clipboard as «class PNGf») to theFile",
"end try",
"close access theFile",
]
script = ["osascript"]
for command in commands:
script += ["-e", command]
subprocess.call(script)
im = None import binascii
if os.stat(filepath).st_size != 0:
im = Image.open(filepath) data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3]))
im.load() return Image.open(data)
os.unlink(filepath)
return im
elif sys.platform == "win32": elif sys.platform == "win32":
fmt, data = Image.core.grabclipboard_win32() fmt, data = Image.core.grabclipboard_win32()
if fmt == "file": # CF_HDROP if fmt == "file": # CF_HDROP

View File

@ -22,7 +22,7 @@ import functools
import operator import operator
import re import re
from collections.abc import Sequence from collections.abc import Sequence
from typing import Protocol, cast from typing import Literal, Protocol, cast, overload
from . import ExifTags, Image, ImagePalette from . import ExifTags, Image, ImagePalette
@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
return _lut(image, lut) return _lut(image, lut)
@overload
def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
@overload
def exif_transpose(
image: Image.Image, *, in_place: Literal[False] = False
) -> Image.Image: ...
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
""" """
If an image has an EXIF Orientation tag, other than 1, transpose the image If an image has an EXIF Orientation tag, other than 1, transpose the image
@ -698,10 +708,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
8: Image.Transpose.ROTATE_90, 8: Image.Transpose.ROTATE_90,
}.get(orientation) }.get(orientation)
if method is not None: if method is not None:
transposed_image = image.transpose(method)
if in_place: if in_place:
image.im = transposed_image.im image.im = image.im.transpose(method)
image._size = transposed_image._size image._size = image.im.size
else:
transposed_image = image.transpose(method)
exif_image = image if in_place else transposed_image exif_image = image if in_place else transposed_image
exif = exif_image.getexif() exif = exif_image.getexif()

View File

@ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile):
"raw", "raw",
(0, 0) + self.size, (0, 0) + self.size,
self.fp.tell() - len(buffer), self.fp.tell() - len(buffer),
(self.mode, 0, 1), self.mode,
) )
] ]

View File

@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
if sig == b"\xff\x4f\xff\x51": if sig == b"\xff\x4f\xff\x51":
self.codec = "j2k" self.codec = "j2k"
self._size, self._mode = _parse_codestream(self.fp) self._size, self._mode = _parse_codestream(self.fp)
self._parse_comment()
else: else:
sig = sig + self.fp.read(8) sig = sig + self.fp.read(8)
@ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
if dpi is not None: if dpi is not None:
self.info["dpi"] = dpi self.info["dpi"] = dpi
if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"):
hdr = self.fp.read(2)
length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR)
self._parse_comment() self._parse_comment()
else: else:
msg = "not a JPEG 2000 file" msg = "not a JPEG 2000 file"
@ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
] ]
def _parse_comment(self) -> None: def _parse_comment(self) -> None:
hdr = self.fp.read(2)
length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR)
while True: while True:
marker = self.fp.read(2) marker = self.fp.read(2)
if not marker: if not marker:

View File

@ -72,7 +72,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
n = i16(self.fp.read(2)) - 2 n = i16(self.fp.read(2)) - 2
s = ImageFile._safe_read(self.fp, n) s = ImageFile._safe_read(self.fp, n)
app = "APP%d" % (marker & 15) app = f"APP{marker & 15}"
self.app[app] = s # compatibility self.app[app] = s # compatibility
self.applist.append((app, s)) self.applist.append((app, s))
@ -90,6 +90,9 @@ def APP(self: JpegImageFile, marker: int) -> None:
else: else:
if jfif_unit == 1: if jfif_unit == 1:
self.info["dpi"] = jfif_density self.info["dpi"] = jfif_density
elif jfif_unit == 2: # cm
# 1 dpcm = 2.54 dpi
self.info["dpi"] = tuple(d * 2.54 for d in jfif_density)
self.info["jfif_unit"] = jfif_unit self.info["jfif_unit"] = jfif_unit
self.info["jfif_density"] = jfif_density self.info["jfif_density"] = jfif_density
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
@ -395,6 +398,13 @@ class JpegImageFile(ImageFile.ImageFile):
return getattr(self, "_" + name) return getattr(self, "_" + name)
raise AttributeError(name) raise AttributeError(name)
def __getstate__(self) -> list[Any]:
return super().__getstate__() + [self.layers, self.layer]
def __setstate__(self, state: list[Any]) -> None:
super().__setstate__(state)
self.layers, self.layer = state[5:]
def load_read(self, read_bytes: int) -> bytes: def load_read(self, read_bytes: int) -> bytes:
""" """
internal: read more image data internal: read more image data
@ -751,7 +761,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
extra = info.get("extra", b"") extra = info.get("extra", b"")
MAX_BYTES_IN_MARKER = 65533 MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp", im.info.get("xmp")) xmp = info.get("xmp")
if xmp: if xmp:
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len

View File

@ -70,9 +70,9 @@ class MspImageFile(ImageFile.ImageFile):
self._size = i16(s, 4), i16(s, 6) self._size = i16(s, 4), i16(s, 6)
if s[:4] == b"DanM": if s[:4] == b"DanM":
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")]
else: else:
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)] self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)]
class MspDecoder(ImageFile.PyDecoder): class MspDecoder(ImageFile.PyDecoder):
@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(o16(h)) fp.write(o16(h))
# image body # image body
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")])
# #

View File

@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile):
self._mode = "RGB" self._mode = "RGB"
self._size = 768, 512 # FIXME: not correct for rotated images! self._size = 768, 512 # FIXME: not correct for rotated images!
self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)] self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)]
def load_end(self) -> None: def load_end(self) -> None:
if self.tile_post_rotate: if self.tile_post_rotate:

View File

@ -86,7 +86,7 @@ class PcxImageFile(ImageFile.ImageFile):
elif bits == 1 and planes in (2, 4): elif bits == 1 and planes in (2, 4):
mode = "P" mode = "P"
rawmode = "P;%dL" % planes rawmode = f"P;{planes}L"
self.palette = ImagePalette.raw("RGB", s[16:64]) self.palette = ImagePalette.raw("RGB", s[16:64])
elif version == 5 and bits == 8 and planes == 1: elif version == 5 and bits == 8 and planes == 1:

View File

@ -61,9 +61,7 @@ class PixarImageFile(ImageFile.ImageFile):
# FIXME: to be continued... # FIXME: to be continued...
# create tile descriptor (assuming "dumped") # create tile descriptor (assuming "dumped")
self.tile = [ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)]
ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))
]
# #

View File

@ -523,7 +523,7 @@ class PngStream(ChunkStream):
assert self.fp is not None assert self.fp is not None
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) raw_vals = struct.unpack(f">{len(s) // 4}I", s)
self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
return s return s

View File

@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile):
self._mode = "RGB" if channels == 3 else "RGBA" self._mode = "RGB" if channels == 3 else "RGBA"
self.fp.seek(1, os.SEEK_CUR) # colorspace self.fp.seek(1, os.SEEK_CUR) # colorspace
self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)] self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())]
class QoiDecoder(ImageFile.PyDecoder): class QoiDecoder(ImageFile.PyDecoder):

View File

@ -154,9 +154,7 @@ class SpiderImageFile(ImageFile.ImageFile):
self.rawmode = "F;32F" self.rawmode = "F;32F"
self._mode = "F" self._mode = "F"
self.tile = [ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)]
ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))
]
self._fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
@property @property
@ -211,26 +209,27 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images # given a list of filenames, return a list of images
def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None:
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1: if filelist is None or len(filelist) < 1:
return None return None
imglist = [] byte_imgs = []
for img in filelist: for img in filelist:
if not os.path.exists(img): if not os.path.exists(img):
print(f"unable to find {img}") print(f"unable to find {img}")
continue continue
try: try:
with Image.open(img) as im: with Image.open(img) as im:
im = im.convert2byte() assert isinstance(im, SpiderImageFile)
byte_im = im.convert2byte()
except Exception: except Exception:
if not isSpiderImage(img): if not isSpiderImage(img):
print(f"{img} is not a Spider image file") print(f"{img} is not a Spider image file")
continue continue
im.info["filename"] = img byte_im.info["filename"] = img
imglist.append(im) byte_imgs.append(byte_im)
return imglist return byte_imgs
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -268,7 +267,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]:
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode[0] != "F": if im.mode != "F":
im = im.convert("F") im = im.convert("F")
hdr = makeSpiderHeader(im) hdr = makeSpiderHeader(im)
@ -280,9 +279,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.writelines(hdr) fp.writelines(hdr)
rawmode = "F;32NF" # 32-bit native floating point rawmode = "F;32NF" # 32-bit native floating point
ImageFile._save( ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

View File

@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __init__( def __init__(
self, self,
ifh: bytes = b"II\052\0\0\0\0\0", ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00",
prefix: bytes | None = None, prefix: bytes | None = None,
group: int | None = None, group: int | None = None,
) -> None: ) -> None:
@ -935,9 +935,9 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tagdata[tag] = data self._tagdata[tag] = data
self.tagtype[tag] = typ self.tagtype[tag] = typ
msg += " - value: " + ( msg += " - value: "
"<table: %d bytes>" % size if size > 32 else repr(data) msg += f"<table: {size} bytes>" if size > 32 else repr(data)
)
logger.debug(msg) logger.debug(msg)
(self.next,) = ( (self.next,) = (
@ -949,12 +949,25 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg)) warnings.warn(str(msg))
return return
def _get_ifh(self) -> bytes:
ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
if self._bigtiff:
ifh += self._pack("HH", 8, 0)
ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
return ifh
def tobytes(self, offset: int = 0) -> bytes: def tobytes(self, offset: int = 0) -> bytes:
# FIXME What about tagdata? # FIXME What about tagdata?
result = self._pack("H", len(self._tags_v2)) result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
entries: list[tuple[int, int, int, bytes, bytes]] = [] entries: list[tuple[int, int, int, bytes, bytes]] = []
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
fmt = "Q" if self._bigtiff else "L"
fmt_size = 8 if self._bigtiff else 4
offset += (
len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
)
stripoffsets = None stripoffsets = None
# pass 1: convert tags to binary format # pass 1: convert tags to binary format
@ -966,11 +979,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict) is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd: if is_ifd:
if self._endian == "<": ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
ifh = b"II\x2A\x00\x08\x00\x00\x00"
else:
ifh = b"MM\x00\x2A\x00\x00\x00\x08"
ifd = ImageFileDirectory_v2(ifh, group=tag)
values = self._tags_v2[tag] values = self._tags_v2[tag]
for ifd_tag, ifd_value in values.items(): for ifd_tag, ifd_value in values.items():
ifd[ifd_tag] = ifd_value ifd[ifd_tag] = ifd_value
@ -981,10 +990,8 @@ class ImageFileDirectory_v2(_IFDv2Base):
tagname = TiffTags.lookup(tag, self.group).name tagname = TiffTags.lookup(tag, self.group).name
typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: "
msg += " - value: " + ( msg += f"<table: {len(data)} bytes>" if len(data) >= 16 else str(values)
"<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
)
logger.debug(msg) logger.debug(msg)
# count is sum of lengths for string and arbitrary data # count is sum of lengths for string and arbitrary data
@ -995,10 +1002,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
else: else:
count = len(values) count = len(values)
# figure out if data fits into the entry # figure out if data fits into the entry
if len(data) <= 4: if len(data) <= fmt_size:
entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
else: else:
entries.append((tag, typ, count, self._pack("L", offset), data)) entries.append((tag, typ, count, self._pack(fmt, offset), data))
offset += (len(data) + 1) // 2 * 2 # pad to word offset += (len(data) + 1) // 2 * 2 # pad to word
# update strip offset data to point beyond auxiliary data # update strip offset data to point beyond auxiliary data
@ -1009,16 +1016,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
values = [val + offset for val in handler(self, data, self.legacy_api)] values = [val + offset for val in handler(self, data, self.legacy_api)]
data = self._write_dispatch[typ](self, *values) data = self._write_dispatch[typ](self, *values)
else: else:
value = self._pack("L", self._unpack("L", value)[0] + offset) value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
entries[stripoffsets] = tag, typ, count, value, data entries[stripoffsets] = tag, typ, count, value, data
# pass 2: write entries to file # pass 2: write entries to file
for tag, typ, count, value, data in entries: for tag, typ, count, value, data in entries:
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
result += self._pack("HHL4s", tag, typ, count, value) result += self._pack(
"HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
)
# -- overwrite here for multi-page -- # -- overwrite here for multi-page --
result += b"\0\0\0\0" # end of entries result += self._pack(fmt, 0) # end of entries
# pass 3: write auxiliary data to file # pass 3: write auxiliary data to file
for tag, typ, count, value, data in entries: for tag, typ, count, value, data in entries:
@ -1030,8 +1039,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def save(self, fp: IO[bytes]) -> int: def save(self, fp: IO[bytes]) -> int:
if fp.tell() == 0: # skip TIFF header on subsequent pages if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._get_ifh())
fp.write(self._prefix + self._pack("HL", 42, 8))
offset = fp.tell() offset = fp.tell()
result = self.tobytes(offset) result = self.tobytes(offset)
@ -1216,10 +1224,6 @@ class TiffImageFile(ImageFile.ImageFile):
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
self.fp = self._fp self.fp = self._fp
# reset buffered io handle in case fp
# was passed to libtiff, invalidating the buffer
self.fp.tell()
while len(self._frame_pos) <= frame: while len(self._frame_pos) <= frame:
if not self.__next: if not self.__next:
msg = "no more images in TIFF file" msg = "no more images in TIFF file"
@ -1303,10 +1307,6 @@ class TiffImageFile(ImageFile.ImageFile):
if not self.is_animated: if not self.is_animated:
self._close_exclusive_fp_after_loading = True self._close_exclusive_fp_after_loading = True
# reset buffered io handle in case fp
# was passed to libtiff, invalidating the buffer
self.fp.tell()
# load IFD data from fp before it is closed # load IFD data from fp before it is closed
exif = self.getexif() exif = self.getexif()
for key in TiffTags.TAGS_V2_GROUPS: for key in TiffTags.TAGS_V2_GROUPS:
@ -1381,8 +1381,17 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("have fileno, calling fileno version of the decoder.") logger.debug("have fileno, calling fileno version of the decoder.")
if not close_self_fp: if not close_self_fp:
self.fp.seek(0) self.fp.seek(0)
# Save and restore the file position, because libtiff will move it
# outside of the Python runtime, and that will confuse
# io.BufferedReader and possible others.
# NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(),
# because the buffer read head already may not equal the actual
# file position, and fp.seek() may just adjust it's internal
# pointer and not actually seek the OS file handle.
pos = os.lseek(fp, 0, os.SEEK_CUR)
# 4 bytes, otherwise the trace might error out # 4 bytes, otherwise the trace might error out
n, err = decoder.decode(b"fpfp") n, err = decoder.decode(b"fpfp")
os.lseek(fp, pos, os.SEEK_SET)
else: else:
# we have something else. # we have something else.
logger.debug("don't have fileno or getvalue. just reading") logger.debug("don't have fileno or getvalue. just reading")
@ -1400,7 +1409,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp = None # might be shared self.fp = None # might be shared
if err < 0: if err < 0:
raise OSError(err) msg = f"decoder error {err}"
raise OSError(msg)
return Image.Image.load(self) return Image.Image.load(self)
@ -1433,8 +1443,12 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
# size # size
xsize = self.tag_v2.get(IMAGEWIDTH) try:
ysize = self.tag_v2.get(IMAGELENGTH) xsize = self.tag_v2[IMAGEWIDTH]
ysize = self.tag_v2[IMAGELENGTH]
except KeyError as e:
msg = "Missing dimensions"
raise TypeError(msg) from e
if not isinstance(xsize, int) or not isinstance(ysize, int): if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions" msg = "Invalid dimensions"
raise ValueError(msg) raise ValueError(msg)
@ -1556,17 +1570,6 @@ class TiffImageFile(ImageFile.ImageFile):
# fillorder==2 modes have a corresponding # fillorder==2 modes have a corresponding
# fillorder=1 mode # fillorder=1 mode
self._mode, rawmode = OPEN_INFO[key] self._mode, rawmode = OPEN_INFO[key]
# libtiff always returns the bytes in native order.
# we're expecting image byte order. So, if the rawmode
# contains I;16, we need to convert from native to image
# byte order.
if rawmode == "I;16":
rawmode = "I;16N"
if ";16B" in rawmode:
rawmode = rawmode.replace(";16B", ";16N")
if ";16L" in rawmode:
rawmode = rawmode.replace(";16L", ";16N")
# YCbCr images with new jpeg compression with pixels in one plane # YCbCr images with new jpeg compression with pixels in one plane
# unpacked straight into RGB values # unpacked straight into RGB values
if ( if (
@ -1575,6 +1578,14 @@ class TiffImageFile(ImageFile.ImageFile):
and self._planar_configuration == 1 and self._planar_configuration == 1
): ):
rawmode = "RGB" rawmode = "RGB"
# libtiff always returns the bytes in native order.
# we're expecting image byte order. So, if the rawmode
# contains I;16, we need to convert from native to image
# byte order.
elif rawmode == "I;16":
rawmode = "I;16N"
elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
rawmode = rawmode[:-1] + "N"
# Offset in the tile tuple is 0, we go from 0,0 to # Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds # w,h, and we only do this once -- eds
@ -1680,10 +1691,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
msg = f"cannot write mode {im.mode} as TIFF" msg = f"cannot write mode {im.mode} as TIFF"
raise OSError(msg) from e raise OSError(msg) from e
ifd = ImageFileDirectory_v2(prefix=prefix)
encoderinfo = im.encoderinfo encoderinfo = im.encoderinfo
encoderconfig = im.encoderconfig encoderconfig = im.encoderconfig
ifd = ImageFileDirectory_v2(prefix=prefix)
if encoderinfo.get("big_tiff"):
ifd._bigtiff = True
try: try:
compression = encoderinfo["compression"] compression = encoderinfo["compression"]
except KeyError: except KeyError:
@ -1915,7 +1929,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if not getattr(Image.core, "libtiff_support_custom_tags", False): if not getattr(Image.core, "libtiff_support_custom_tags", False):
continue continue
if tag in ifd.tagtype: if tag in TiffTags.TAGS_V2_GROUPS:
types[tag] = TiffTags.LONG8
elif tag in ifd.tagtype:
types[tag] = ifd.tagtype[tag] types[tag] = ifd.tagtype[tag]
elif not (isinstance(value, (int, float, str, bytes))): elif not (isinstance(value, (int, float, str, bytes))):
continue continue
@ -2031,20 +2047,21 @@ class AppendingTiffWriter(io.BytesIO):
self.offsetOfNewPage = 0 self.offsetOfNewPage = 0
self.IIMM = iimm = self.f.read(4) self.IIMM = iimm = self.f.read(4)
self._bigtiff = b"\x2B" in iimm
if not iimm: if not iimm:
# empty file - first page # empty file - first page
self.isFirst = True self.isFirst = True
return return
self.isFirst = False self.isFirst = False
if iimm == b"II\x2a\x00": if iimm not in PREFIXES:
self.setEndian("<")
elif iimm == b"MM\x00\x2a":
self.setEndian(">")
else:
msg = "Invalid TIFF file header" msg = "Invalid TIFF file header"
raise RuntimeError(msg) raise RuntimeError(msg)
self.setEndian("<" if iimm.startswith(II) else ">")
if self._bigtiff:
self.f.seek(4, os.SEEK_CUR)
self.skipIFDs() self.skipIFDs()
self.goToEnd() self.goToEnd()
@ -2064,11 +2081,13 @@ class AppendingTiffWriter(io.BytesIO):
msg = "IIMM of new page doesn't match IIMM of first page" msg = "IIMM of new page doesn't match IIMM of first page"
raise RuntimeError(msg) raise RuntimeError(msg)
ifd_offset = self.readLong() if self._bigtiff:
self.f.seek(4, os.SEEK_CUR)
ifd_offset = self._read(8 if self._bigtiff else 4)
ifd_offset += self.offsetOfNewPage ifd_offset += self.offsetOfNewPage
assert self.whereToWriteNewIFDOffset is not None assert self.whereToWriteNewIFDOffset is not None
self.f.seek(self.whereToWriteNewIFDOffset) self.f.seek(self.whereToWriteNewIFDOffset)
self.writeLong(ifd_offset) self._write(ifd_offset, 8 if self._bigtiff else 4)
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
self.fixIFD() self.fixIFD()
@ -2114,18 +2133,20 @@ class AppendingTiffWriter(io.BytesIO):
self.endian = endian self.endian = endian
self.longFmt = f"{self.endian}L" self.longFmt = f"{self.endian}L"
self.shortFmt = f"{self.endian}H" self.shortFmt = f"{self.endian}H"
self.tagFormat = f"{self.endian}HHL" self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
def skipIFDs(self) -> None: def skipIFDs(self) -> None:
while True: while True:
ifd_offset = self.readLong() ifd_offset = self._read(8 if self._bigtiff else 4)
if ifd_offset == 0: if ifd_offset == 0:
self.whereToWriteNewIFDOffset = self.f.tell() - 4 self.whereToWriteNewIFDOffset = self.f.tell() - (
8 if self._bigtiff else 4
)
break break
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
num_tags = self.readShort() num_tags = self._read(8 if self._bigtiff else 2)
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
def write(self, data: Buffer, /) -> int: def write(self, data: Buffer, /) -> int:
return self.f.write(data) return self.f.write(data)
@ -2155,17 +2176,19 @@ class AppendingTiffWriter(io.BytesIO):
msg = f"wrote only {bytes_written} bytes but wanted {expected}" msg = f"wrote only {bytes_written} bytes but wanted {expected}"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastShortToLong(self, value: int) -> None: def _rewriteLast(
self.f.seek(-2, os.SEEK_CUR) self, value: int, field_size: int, new_field_size: int = 0
bytes_written = self.f.write(struct.pack(self.longFmt, value)) ) -> None:
self._verify_bytes_written(bytes_written, 4)
def _rewriteLast(self, value: int, field_size: int) -> None:
self.f.seek(-field_size, os.SEEK_CUR) self.f.seek(-field_size, os.SEEK_CUR)
if not new_field_size:
new_field_size = field_size
bytes_written = self.f.write( bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value) struct.pack(self.endian + self._fmt(new_field_size), value)
) )
self._verify_bytes_written(bytes_written, field_size) self._verify_bytes_written(bytes_written, new_field_size)
def rewriteLastShortToLong(self, value: int) -> None:
self._rewriteLast(value, 2, 4)
def rewriteLastShort(self, value: int) -> None: def rewriteLastShort(self, value: int) -> None:
return self._rewriteLast(value, 2) return self._rewriteLast(value, 2)
@ -2173,13 +2196,17 @@ class AppendingTiffWriter(io.BytesIO):
def rewriteLastLong(self, value: int) -> None: def rewriteLastLong(self, value: int) -> None:
return self._rewriteLast(value, 4) return self._rewriteLast(value, 4)
def _write(self, value: int, field_size: int) -> None:
bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value)
)
self._verify_bytes_written(bytes_written, field_size)
def writeShort(self, value: int) -> None: def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) self._write(value, 2)
self._verify_bytes_written(bytes_written, 2)
def writeLong(self, value: int) -> None: def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value)) self._write(value, 4)
self._verify_bytes_written(bytes_written, 4)
def close(self) -> None: def close(self) -> None:
self.finalize() self.finalize()
@ -2187,24 +2214,37 @@ class AppendingTiffWriter(io.BytesIO):
self.f.close() self.f.close()
def fixIFD(self) -> None: def fixIFD(self) -> None:
num_tags = self.readShort() num_tags = self._read(8 if self._bigtiff else 2)
for i in range(num_tags): for i in range(num_tags):
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) tag, field_type, count = struct.unpack(
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
)
field_size = self.fieldSizes[field_type] field_size = self.fieldSizes[field_type]
total_size = field_size * count total_size = field_size * count
is_local = total_size <= 4 fmt_size = 8 if self._bigtiff else 4
is_local = total_size <= fmt_size
if not is_local: if not is_local:
offset = self.readLong() + self.offsetOfNewPage offset = self._read(fmt_size) + self.offsetOfNewPage
self.rewriteLastLong(offset) self._rewriteLast(offset, fmt_size)
if tag in self.Tags: if tag in self.Tags:
cur_pos = self.f.tell() cur_pos = self.f.tell()
logger.debug(
"fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d",
TiffTags.lookup(tag).name,
tag,
TYPES.get(field_type, "unknown"),
field_type,
field_size,
count,
)
if is_local: if is_local:
self._fixOffsets(count, field_size) self._fixOffsets(count, field_size)
self.f.seek(cur_pos + 4) self.f.seek(cur_pos + fmt_size)
else: else:
self.f.seek(offset) self.f.seek(offset)
self._fixOffsets(count, field_size) self._fixOffsets(count, field_size)
@ -2212,24 +2252,33 @@ class AppendingTiffWriter(io.BytesIO):
elif is_local: elif is_local:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(fmt_size, os.SEEK_CUR)
def _fixOffsets(self, count: int, field_size: int) -> None: def _fixOffsets(self, count: int, field_size: int) -> None:
for i in range(count): for i in range(count):
offset = self._read(field_size) offset = self._read(field_size)
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
if field_size == 2 and offset >= 65536:
# offset is now too large - we must convert shorts to longs new_field_size = 0
if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
# offset is now too large - we must convert long to long8
new_field_size = 8
elif field_size == 2 and offset >= 2**16:
# offset is now too large - we must convert short to long
new_field_size = 4
if new_field_size:
if count != 1: if count != 1:
msg = "not implemented" msg = "not implemented"
raise RuntimeError(msg) # XXX TODO raise RuntimeError(msg) # XXX TODO
# simple case - the offset is just one and therefore it is # simple case - the offset is just one and therefore it is
# local (not referenced with another offset) # local (not referenced with another offset)
self.rewriteLastShortToLong(offset) self._rewriteLast(offset, field_size, new_field_size)
self.f.seek(-10, os.SEEK_CUR) # Move back past the new offset, past 'count', and before 'field_type'
self.writeShort(TiffTags.LONG) # rewrite the type to LONG rewind = -new_field_size - 4 - 2
self.f.seek(8, os.SEEK_CUR) self.f.seek(rewind, os.SEEK_CUR)
self.writeShort(new_field_size) # rewrite the type
self.f.seek(2 - rewind, os.SEEK_CUR)
else: else:
self._rewriteLast(offset, field_size) self._rewriteLast(offset, field_size)

View File

@ -92,6 +92,9 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# get units per inch # get units per inch
self._inch = word(s, 14) self._inch = word(s, 14)
if self._inch == 0:
msg = "Invalid inch"
raise ValueError(msg)
# get bounding box # get bounding box
x0 = short(s, 6) x0 = short(s, 6)

View File

@ -74,9 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile):
self.palette = ImagePalette.raw("RGB", PALETTE) self.palette = ImagePalette.raw("RGB", PALETTE)
self.tile = [ self.tile = [
ImageFile._Tile( ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode)
"raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1)
)
] ]

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