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
python3 -m pip install coverage
if [[ $MATRIX_DOCKER ]]; then
python3 -m coverage xml --ignore-errors
else
python3 -m coverage xml
fi
python3 -m coverage xml

View File

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

View File

@ -21,7 +21,7 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then
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\
sway wl-clipboard libopenblas-dev
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-PySide6
ipython

View File

@ -19,7 +19,6 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Follow PEP 8.
- 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.
- 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

View File

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

View File

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

View File

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

View File

@ -44,6 +44,7 @@ jobs:
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-40-amd64,
@ -89,18 +90,18 @@ jobs:
- name: After success
run: |
PATH="$PATH:~/.local/bin"
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__)))'`
docker stop pillow_container
sudo mkdir -p $pil_path
sudo cp src/PIL/*.py $pil_path
cd /Pillow
.ci/after_success.sh
env:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: GHA_Docker
name: ${{ matrix.docker }}

View File

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

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
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"]
os: ["windows-latest"]
include:
@ -221,9 +221,9 @@ jobs:
shell: pwsh
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
files: ./coverage.xml
flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -42,6 +42,8 @@ jobs:
]
python-version: [
"pypy3.10",
"3.14",
"3.13t",
"3.13",
"3.12",
"3.11",
@ -52,14 +54,14 @@ jobs:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# 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+
- { os: "macos-13", python-version: "3.9" }
exclude:
- { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
@ -67,8 +69,7 @@ jobs:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
if: "${{ !matrix.disable-gil }}"
uses: Quansight-Labs/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@ -77,13 +78,6 @@ jobs:
".ci/*.sh"
"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
if: "${{ matrix.disable-gil }}"
run: |
@ -116,7 +110,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- 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"
- name: Build
@ -156,7 +150,7 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}

View File

@ -1,11 +1,33 @@
#!/bin/bash
# Define custom utilities
# Test for macOS with [ -n "$IS_MACOS" ]
if [ -z "$IS_MACOS" ]; then
export MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
export MB_ML_VER=${AUDITWHEEL_POLICY:9}
# Setup that needs to be done before multibuild utils are invoked
PROJECTDIR=$(pwd)
if [[ "$(uname -s)" == "Darwin" ]]; then
# 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
export PLAT=$CIBW_ARCHS
PLAT=$CIBW_ARCHS
# Define custom utilities
source wheels/multibuild/common_utils.sh
source wheels/multibuild/library_builders.sh
if [ -z "$IS_MACOS" ]; then
@ -15,79 +37,95 @@ fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=10.0.1
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.0.4
OPENJPEG_VERSION=2.5.2
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.45
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
GIFLIB_VERSION=5.2.2
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
ZLIB_NG_VERSION=2.2.3
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.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 {
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)
(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)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libbrotli* /usr/local/lib
cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
fi
touch brotli-stamp
}
function build_harfbuzz {
if [ -e harfbuzz-stamp ]; then return; fi
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 \
&& 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 \
&& meson install)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libharfbuzz* /usr/local/lib
fi
touch harfbuzz-stamp
}
function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
sudo chown -R runner /usr/local
fi
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
fi
build_new_zlib
build_zlib_ng
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
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
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
fi
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
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
build_libjpeg_turbo
build_tiff
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
fi
build_libpng
build_lcms2
build_openjpeg
@ -97,7 +135,9 @@ function build {
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
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
build_brotli
@ -112,32 +152,47 @@ function build {
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
# Runs in the root directory of this repository.
curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
untar pillow-depends-main.zip
if [[ ! -d $WORKDIR/pillow-depends-main ]]; then
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
# libdeflate may cause a minimum target error when repairing the wheel
# libtiff and libxcb cause a conflict with building libtiff and libxcb
# libxau and libxdmcp cause an issue on macOS < 11
# remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64
# remove jpeg-turbo to avoid inclusion on arm64
# remove webp and zstd to avoid inclusion on x86_64
# curl from brew requires zstd, use system curl
brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
brew remove --ignore-dependencies jpeg-turbo
else
brew remove --ignore-dependencies libdeflate webp
fi
# Homebrew (or similar packaging environments) install can contain some of
# the libraries that we're going to build. However, they may be compiled
# with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use,
# and they may bring in other dependencies that we don't want. The same will
# be true of any other locations on the path. To avoid conflicts, strip the
# path down to the bare minimum (which, on macOS, won't include any
# development dependencies).
export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
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
wrap_wheel_builder build
# Return to the project root to finish the build
popd > /dev/null
# Append licenses
for filename in wheels/dependency_licenses/*; do
echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE

View File

@ -1,12 +1,24 @@
#!/bin/bash
set -e
# Ensure fribidi is installed by the system.
if [[ "$OSTYPE" == "darwin"* ]]; then
brew install fribidi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then
sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib
# If Homebrew is on the path during the build, it may leak into the wheels.
# However, we *do* need Homebrew to provide a copy of fribidi for
# testing purposes so that we can verify the fribidi shim works as expected.
if [[ "$(uname -m)" == "x86_64" ]]; then
HOMEBREW_PREFIX=/usr/local
else
HOMEBREW_PREFIX=/opt/homebrew
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
apk add curl fribidi
else

View File

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

5
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

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)
- [Pull requests](https://github.com/python-pillow/Pillow/pulls)
- [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)
## 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 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`
* [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
* [ ] Create branch and tag for release e.g.:
```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.
* [ ] Make necessary changes in `main` branch.
* [ ] Update `CHANGES.rst`.
* [ ] Check out release branch e.g.:
```bash
git checkout -t remotes/origin/5.2.x

View File

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

View File

@ -140,18 +140,11 @@ def assert_image_similar_tofile(
filename: str,
epsilon: float,
msg: str | None = None,
mode: str | None = None,
) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
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:
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 pytest
from PIL import Image, UnidentifiedImageError, features
from PIL import Image, features
from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"):
@ -32,21 +32,17 @@ def test_fuzz_images(path: str) -> None:
fuzzers.fuzz_image(f.read())
assert True
except (
# Known exceptions from Pillow
OSError,
SyntaxError,
MemoryError,
ValueError,
NotImplementedError,
OverflowError,
):
# Known exceptions that are through from Pillow
assert True
except (
# Known Image.* exceptions
Image.DecompressionBombError,
Image.DecompressionBombWarning,
UnidentifiedImageError,
):
# Known Image.* exceptions
assert True
finally:
fuzzers.disable_decompressionbomb_error()

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from PIL import Image
from PIL import BlpImagePlugin, Image
from .helper import (
assert_image_equal,
@ -19,6 +19,7 @@ def test_load_blp1() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
assert im.mode == "RGBA"
im.load()
@ -37,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
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:
f = str(tmp_path / "temp.blp")

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import warnings
from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from typing import Any
import pytest
@ -1435,7 +1436,8 @@ def test_saving_rgba(tmp_path: Path) -> None:
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")
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.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:
assert reloaded.n_frames == 2

View File

@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None:
im.save(temp_file)
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)
assert handler.saved
Hdf5StubImagePlugin._handler = None
Hdf5StubImagePlugin.register_handler(None)

View File

@ -253,8 +253,7 @@ def test_truncated_mask() -> None:
try:
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
output = io.BytesIO()

View File

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

View File

@ -181,6 +181,10 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200)
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(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@ -277,7 +281,10 @@ class TestFileJpeg:
assert not im2.info.get("progressive")
assert im3.info.get("progressive")
assert_image_equal(im1, im3)
if features.check_feature("mozjpeg"):
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im3)
assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
@ -349,7 +356,6 @@ class TestFileJpeg:
assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im)
assert transposed is not None
exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {}
@ -420,8 +426,12 @@ class TestFileJpeg:
im2 = self.roundtrip(hopper(), progressive=1)
im3 = self.roundtrip(hopper(), progression=1) # compatibility
assert_image_equal(im1, im2)
assert_image_equal(im1, im3)
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, im3)
assert im2.info.get("progressive")
assert im2.info.get("progression")
assert im3.info.get("progressive")
@ -1000,8 +1010,13 @@ class TestFileJpeg:
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"
im.info["xmp"] = b"1" * 65504
im.save(f)
# Check that XMP is not saved from image info
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:
assert reloaded.info["xmp"] == b"1" * 65504
@ -1022,7 +1037,7 @@ class TestFileJpeg:
with Image.open(TEST_FILE) as im:
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
im.load()

View File

@ -325,6 +325,18 @@ def test_cmyk() -> None:
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"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im:
@ -424,8 +436,9 @@ def test_pclr() -> None:
def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
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"
# Test an image that is truncated partway through a codestream
with open("Tests/images/comment.jp2", "rb") as fp:
@ -479,8 +492,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None:
out.seek(0)
while True:
marker = out.read(2)
if not marker:
pytest.fail("End of stream without PLT")
assert marker, "End of stream without PLT"
jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F:

View File

@ -36,11 +36,7 @@ class LibTiffTestCase:
im.load()
im.getdata()
try:
assert im._compression == "group4"
except AttributeError:
print("No _compression")
print(dir(im))
assert im._compression == "group4"
# can we write it back out, in a different form.
out = str(tmp_path / "temp.png")
@ -1098,6 +1094,25 @@ class TestFileLibTiff(LibTiffTestCase):
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")
def test_sampleformat_not_corrupted(self) -> None:
# Assert that a TIFF image with SampleFormat=UINT tag is not corrupted
@ -1127,7 +1142,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.load()
# 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"))
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
jpg = roundtrip(im, save_all=True)
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:
# The file is truncated
with pytest.raises(OSError):
im.text()
im.text
ImageFile.LOAD_TRUNCATED_IMAGES = True
assert isinstance(im.text, dict)
ImageFile.LOAD_TRUNCATED_IMAGES = False
@ -772,22 +772,18 @@ class TestFilePng:
im.seek(1)
@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = 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:
im.save(sys.stdout, "PNG")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
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))
def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:

View File

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

View File

@ -115,6 +115,19 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
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:
with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif")
@ -733,7 +746,7 @@ class TestFileTiff:
assert reread.n_frames == 3
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:
b.seek(0)
a.fixOffsets(1, isShort=True)
@ -746,6 +759,37 @@ class TestFileTiff:
with pytest.raises(RuntimeError):
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:
# Tests saving TIFF with icc_profile set.
# 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)
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:
class TestHandler(ImageFile.StubHandler):
methodCalled = False

View File

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

View File

@ -271,13 +271,25 @@ class TestImagePutPixelError:
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")
def test_embeddable(self) -> None:
import ctypes
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:
home = sys.prefix.replace("\\", "\\\\")
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"])
compiler.link_executable(objects, "embed_pil")

View File

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

View File

@ -104,20 +104,20 @@ def test_transposed() -> None:
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
with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft
original_draft = 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:
result = draft(mode, size)
result = original_draft(mode, size)
assert result is not None
return result
im.draft = im_draft
monkeypatch.setattr(im, "draft", im_draft)
im.thumbnail((64, 64))

View File

@ -1674,6 +1674,9 @@ def test_continuous_horizontal_edges_polygon() -> None:
def test_discontiguous_corners_polygon() -> None:
img, draw = create_base_image_draw((84, 68))
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(
((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 (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")
def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p:

View File

@ -405,7 +405,6 @@ def test_exif_transpose() -> None:
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im:
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
transposed_im2 = ImageOps.exif_transpose(transposed_im)
assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im)
check(base_im)
@ -433,7 +431,6 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif()
@ -446,14 +443,12 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif
im = hopper()
im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
@ -464,7 +459,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
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)
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:
# Arrange
filename = str(tmp_path / "temp.pkl")

View File

@ -1,7 +1,7 @@
# Documentation: https://docs.codecov.com/docs/codecov-yaml
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://docs.codecov.com/docs/comparing-commits
allow_coverage_offsets: true

View File

@ -1,7 +1,7 @@
#!/bin/bash
# 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

View File

@ -1,7 +1,7 @@
#!/bin/bash
# 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

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) 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
Software License:

View File

@ -55,7 +55,7 @@ master_doc = "index"
project = "Pillow (PIL Fork)"
copyright = (
"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)"

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
``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
----------------

View File

@ -572,10 +572,19 @@ JPEG 2000
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
``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
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
(``.jp2`` or ``.jpx`` files).
.. versionadded:: 8.3.0
Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
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
: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
**big_tiff**
If true, the image will be saved as a BigTIFF.
.. versionadded:: 11.1.0
**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression

View File

@ -678,7 +678,7 @@ Reading from URL
from PIL import Image
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))

View File

@ -58,7 +58,7 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality.
* 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
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
<https://brew.sh/>`_. After you install Homebrew, run::
brew install libjpeg 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.
brew install libjpeg libraqm libtiff little-cms2 openjpeg webp
.. tab:: Windows
@ -195,11 +189,6 @@ Many of Pillow's features require external libraries:
mingw-w64-x86_64-libimagequant \
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
.. 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 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, 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 |
| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 |
| | 3.12 (MinGW) | 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 |
| | | 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 | |
+----------------------------------+----------------------------+------------------+--------------+

View File

@ -54,6 +54,7 @@ Feature version numbers are available only where stated.
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.
* ``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.
* ``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.

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::
:maxdepth: 2
11.1.0
11.0.0
10.4.0
10.3.0

View File

@ -73,10 +73,10 @@ optional-dependencies.typing = [
optional-dependencies.xmp = [
"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.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."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
urls.Source = "https://github.com/python-pillow/Pillow"
@ -94,10 +94,18 @@ version = { attr = "PIL.__version__" }
[tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
# 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-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]
exclude = "wheels/multibuild"

View File

@ -344,7 +344,7 @@ class pil_build_ext(build_ext):
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"),
]
+ [("add-imaging-libs=", None, "Add libs to _imaging build")]
@ -393,13 +393,14 @@ class pil_build_ext(build_ext):
self.feature.required.discard(x)
_dbg("Disabling %s", 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)
if x == "freetype":
_dbg("--disable-freetype implies --disable-raqm")
_dbg("'-C freetype=disable' implies '-C raqm=disable'")
if getattr(self, "enable_raqm"):
msg = (
"Conflicting options: --enable-raqm and --disable-freetype"
"Conflicting options: "
"'-C raqm=enable' and '-C freetype=disable'"
)
raise ValueError(msg)
setattr(self, "disable_raqm", True)
@ -407,15 +408,17 @@ class pil_build_ext(build_ext):
_dbg("Requiring %s", x)
self.feature.required.add(x)
if x == "raqm":
_dbg("--enable-raqm implies --enable-freetype")
_dbg("'-C raqm=enable' implies '-C freetype=enable'")
self.feature.required.add("freetype")
for x in ("raqm", "fribidi"):
if getattr(self, f"vendor_{x}"):
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)
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)
_dbg("Using vendored version of %s", x)
self.feature.vendor.add(x)
@ -448,7 +451,7 @@ class pil_build_ext(build_ext):
def get_macos_sdk_path(self) -> str | None:
try:
sdk_path = (
subprocess.check_output(["xcrun", "--show-sdk-path"])
subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"])
.strip()
.decode("latin1")
)
@ -606,6 +609,7 @@ class pil_build_ext(build_ext):
_add_directory(library_dirs, "/usr/X11/lib")
_add_directory(include_dirs, "/usr/X11/include")
# Add the macOS SDK path.
sdk_path = self.get_macos_sdk_path()
if sdk_path:
_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")
elif sys.platform == "win32" and _find_library_file(self, "zlib"):
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"):
_dbg("Looking for jpeg")
@ -1044,7 +1050,7 @@ except DependencyException as err:
msg = f"""
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)

View File

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

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("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
)
ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
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"):
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"grestore end\n")

View File

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

View File

@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile):
framesize = i32(s)
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

View File

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

View File

@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile):
self._mode = "RGBA"
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
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:
msg = f"Invalid texture compression format: {repr(format)}"
raise ValueError(msg)

View File

@ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile):
"raw",
(0, 0) + self.size,
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_im = Image.new("P", im_frame.size, background)
assert im_frames[0].im.palette is not None
background_im.putpalette(im_frames[0].im.palette)
first_palette = 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]
elif encoderinfo.get("optimize") and im_frame.mode != "1":
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])
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"))
if im.mode in ["P", "PA"]:
fp.write(b"Lut: 1\r\n")

View File

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

View File

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

View File

@ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter):
ch_out = channels or ch_in
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_out = 0
for b in range(size_3d):

View File

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

View File

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

View File

@ -22,7 +22,7 @@ import functools
import operator
import re
from collections.abc import Sequence
from typing import Protocol, cast
from typing import Literal, Protocol, cast, overload
from . import ExifTags, Image, ImagePalette
@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
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:
"""
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,
}.get(orientation)
if method is not None:
transposed_image = image.transpose(method)
if in_place:
image.im = transposed_image.im
image._size = transposed_image._size
image.im = image.im.transpose(method)
image._size = image.im.size
else:
transposed_image = image.transpose(method)
exif_image = image if in_place else transposed_image
exif = exif_image.getexif()

View File

@ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile):
"raw",
(0, 0) + self.size,
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":
self.codec = "j2k"
self._size, self._mode = _parse_codestream(self.fp)
self._parse_comment()
else:
sig = sig + self.fp.read(8)
@ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
if dpi is not None:
self.info["dpi"] = dpi
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()
else:
msg = "not a JPEG 2000 file"
@ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
]
def _parse_comment(self) -> None:
hdr = self.fp.read(2)
length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR)
while True:
marker = self.fp.read(2)
if not marker:

View File

@ -72,7 +72,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
n = i16(self.fp.read(2)) - 2
s = ImageFile._safe_read(self.fp, n)
app = "APP%d" % (marker & 15)
app = f"APP{marker & 15}"
self.app[app] = s # compatibility
self.applist.append((app, s))
@ -90,6 +90,9 @@ def APP(self: JpegImageFile, marker: int) -> None:
else:
if jfif_unit == 1:
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_density"] = jfif_density
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
@ -395,6 +398,13 @@ class JpegImageFile(ImageFile.ImageFile):
return getattr(self, "_" + 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:
"""
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"")
MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp", im.info.get("xmp"))
xmp = info.get("xmp")
if xmp:
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
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)
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:
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):
@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(o16(h))
# 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._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:
if self.tile_post_rotate:

View File

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

View File

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

View File

@ -523,7 +523,7 @@ class PngStream(ChunkStream):
assert self.fp is not None
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)
return s

View File

@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile):
self._mode = "RGB" if channels == 3 else "RGBA"
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):

View File

@ -154,9 +154,7 @@ class SpiderImageFile(ImageFile.ImageFile):
self.rawmode = "F;32F"
self._mode = "F"
self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))
]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)]
self._fp = self.fp # FIXME: hack
@property
@ -211,26 +209,27 @@ class SpiderImageFile(ImageFile.ImageFile):
# 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"""
if filelist is None or len(filelist) < 1:
return None
imglist = []
byte_imgs = []
for img in filelist:
if not os.path.exists(img):
print(f"unable to find {img}")
continue
try:
with Image.open(img) as im:
im = im.convert2byte()
assert isinstance(im, SpiderImageFile)
byte_im = im.convert2byte()
except Exception:
if not isSpiderImage(img):
print(f"{img} is not a Spider image file")
continue
im.info["filename"] = img
imglist.append(im)
return imglist
byte_im.info["filename"] = img
byte_imgs.append(byte_im)
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:
if im.mode[0] != "F":
if im.mode != "F":
im = im.convert("F")
hdr = makeSpiderHeader(im)
@ -280,9 +279,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.writelines(hdr)
rawmode = "F;32NF" # 32-bit native floating point
ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
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__(
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,
group: int | None = None,
) -> None:
@ -935,9 +935,9 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tagdata[tag] = data
self.tagtype[tag] = typ
msg += " - value: " + (
"<table: %d bytes>" % size if size > 32 else repr(data)
)
msg += " - value: "
msg += f"<table: {size} bytes>" if size > 32 else repr(data)
logger.debug(msg)
(self.next,) = (
@ -949,12 +949,25 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg))
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:
# 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]] = []
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
# 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))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd:
if self._endian == "<":
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)
ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
values = self._tags_v2[tag]
for ifd_tag, ifd_value in values.items():
ifd[ifd_tag] = ifd_value
@ -981,10 +990,8 @@ class ImageFileDirectory_v2(_IFDv2Base):
tagname = TiffTags.lookup(tag, self.group).name
typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})"
msg += " - value: " + (
"<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
)
msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: "
msg += f"<table: {len(data)} bytes>" if len(data) >= 16 else str(values)
logger.debug(msg)
# count is sum of lengths for string and arbitrary data
@ -995,10 +1002,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
else:
count = len(values)
# figure out if data fits into the entry
if len(data) <= 4:
entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
if len(data) <= fmt_size:
entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
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
# 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)]
data = self._write_dispatch[typ](self, *values)
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
# pass 2: write entries to file
for tag, typ, count, value, data in entries:
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 --
result += b"\0\0\0\0" # end of entries
result += self._pack(fmt, 0) # end of entries
# pass 3: write auxiliary data to file
for tag, typ, count, value, data in entries:
@ -1030,8 +1039,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def save(self, fp: IO[bytes]) -> int:
if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8
fp.write(self._prefix + self._pack("HL", 42, 8))
fp.write(self._get_ifh())
offset = fp.tell()
result = self.tobytes(offset)
@ -1216,10 +1224,6 @@ class TiffImageFile(ImageFile.ImageFile):
def _seek(self, frame: int) -> None:
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:
if not self.__next:
msg = "no more images in TIFF file"
@ -1303,10 +1307,6 @@ class TiffImageFile(ImageFile.ImageFile):
if not self.is_animated:
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
exif = self.getexif()
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.")
if not close_self_fp:
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
n, err = decoder.decode(b"fpfp")
os.lseek(fp, pos, os.SEEK_SET)
else:
# we have something else.
logger.debug("don't have fileno or getvalue. just reading")
@ -1400,7 +1409,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp = None # might be shared
if err < 0:
raise OSError(err)
msg = f"decoder error {err}"
raise OSError(msg)
return Image.Image.load(self)
@ -1433,8 +1443,12 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
# size
xsize = self.tag_v2.get(IMAGEWIDTH)
ysize = self.tag_v2.get(IMAGELENGTH)
try:
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):
msg = "Invalid dimensions"
raise ValueError(msg)
@ -1556,17 +1570,6 @@ class TiffImageFile(ImageFile.ImageFile):
# fillorder==2 modes have a corresponding
# fillorder=1 mode
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
# unpacked straight into RGB values
if (
@ -1575,6 +1578,14 @@ class TiffImageFile(ImageFile.ImageFile):
and self._planar_configuration == 1
):
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
# 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"
raise OSError(msg) from e
ifd = ImageFileDirectory_v2(prefix=prefix)
encoderinfo = im.encoderinfo
encoderconfig = im.encoderconfig
ifd = ImageFileDirectory_v2(prefix=prefix)
if encoderinfo.get("big_tiff"):
ifd._bigtiff = True
try:
compression = encoderinfo["compression"]
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):
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]
elif not (isinstance(value, (int, float, str, bytes))):
continue
@ -2031,20 +2047,21 @@ class AppendingTiffWriter(io.BytesIO):
self.offsetOfNewPage = 0
self.IIMM = iimm = self.f.read(4)
self._bigtiff = b"\x2B" in iimm
if not iimm:
# empty file - first page
self.isFirst = True
return
self.isFirst = False
if iimm == b"II\x2a\x00":
self.setEndian("<")
elif iimm == b"MM\x00\x2a":
self.setEndian(">")
else:
if iimm not in PREFIXES:
msg = "Invalid TIFF file header"
raise RuntimeError(msg)
self.setEndian("<" if iimm.startswith(II) else ">")
if self._bigtiff:
self.f.seek(4, os.SEEK_CUR)
self.skipIFDs()
self.goToEnd()
@ -2064,11 +2081,13 @@ class AppendingTiffWriter(io.BytesIO):
msg = "IIMM of new page doesn't match IIMM of first page"
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
assert self.whereToWriteNewIFDOffset is not None
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.fixIFD()
@ -2114,18 +2133,20 @@ class AppendingTiffWriter(io.BytesIO):
self.endian = endian
self.longFmt = f"{self.endian}L"
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:
while True:
ifd_offset = self.readLong()
ifd_offset = self._read(8 if self._bigtiff else 4)
if ifd_offset == 0:
self.whereToWriteNewIFDOffset = self.f.tell() - 4
self.whereToWriteNewIFDOffset = self.f.tell() - (
8 if self._bigtiff else 4
)
break
self.f.seek(ifd_offset)
num_tags = self.readShort()
self.f.seek(num_tags * 12, os.SEEK_CUR)
num_tags = self._read(8 if self._bigtiff else 2)
self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
def write(self, data: Buffer, /) -> int:
return self.f.write(data)
@ -2155,17 +2176,19 @@ class AppendingTiffWriter(io.BytesIO):
msg = f"wrote only {bytes_written} bytes but wanted {expected}"
raise RuntimeError(msg)
def rewriteLastShortToLong(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
self._verify_bytes_written(bytes_written, 4)
def _rewriteLast(self, value: int, field_size: int) -> None:
def _rewriteLast(
self, value: int, field_size: int, new_field_size: int = 0
) -> None:
self.f.seek(-field_size, os.SEEK_CUR)
if not new_field_size:
new_field_size = field_size
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:
return self._rewriteLast(value, 2)
@ -2173,13 +2196,17 @@ class AppendingTiffWriter(io.BytesIO):
def rewriteLastLong(self, value: int) -> None:
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:
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
self._verify_bytes_written(bytes_written, 2)
self._write(value, 2)
def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value))
self._verify_bytes_written(bytes_written, 4)
self._write(value, 4)
def close(self) -> None:
self.finalize()
@ -2187,24 +2214,37 @@ class AppendingTiffWriter(io.BytesIO):
self.f.close()
def fixIFD(self) -> None:
num_tags = self.readShort()
num_tags = self._read(8 if self._bigtiff else 2)
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]
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:
offset = self.readLong() + self.offsetOfNewPage
self.rewriteLastLong(offset)
offset = self._read(fmt_size) + self.offsetOfNewPage
self._rewriteLast(offset, fmt_size)
if tag in self.Tags:
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:
self._fixOffsets(count, field_size)
self.f.seek(cur_pos + 4)
self.f.seek(cur_pos + fmt_size)
else:
self.f.seek(offset)
self._fixOffsets(count, field_size)
@ -2212,24 +2252,33 @@ class AppendingTiffWriter(io.BytesIO):
elif is_local:
# 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:
for i in range(count):
offset = self._read(field_size)
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:
msg = "not implemented"
raise RuntimeError(msg) # XXX TODO
# simple case - the offset is just one and therefore it is
# local (not referenced with another offset)
self.rewriteLastShortToLong(offset)
self.f.seek(-10, os.SEEK_CUR)
self.writeShort(TiffTags.LONG) # rewrite the type to LONG
self.f.seek(8, os.SEEK_CUR)
self._rewriteLast(offset, field_size, new_field_size)
# Move back past the new offset, past 'count', and before 'field_type'
rewind = -new_field_size - 4 - 2
self.f.seek(rewind, os.SEEK_CUR)
self.writeShort(new_field_size) # rewrite the type
self.f.seek(2 - rewind, os.SEEK_CUR)
else:
self._rewriteLast(offset, field_size)

View File

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

View File

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

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