Merge branch 'python-pillow:main' into usepcf

This commit is contained in:
fjhenigman 2026-03-06 22:00:52 -05:00 committed by GitHub
commit 97bdfeb4a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 356 additions and 240 deletions

View File

@ -53,7 +53,7 @@ pushd depends && ./install_imagequant.sh && popd
pushd depends && sudo ./install_raqm.sh && popd
# libavif
pushd depends && sudo ./install_libavif.sh && popd
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -48,6 +48,13 @@ jobs:
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Cache libavif
uses: actions/cache@v5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
uses: actions/cache@v5
id: cache-libimagequant
@ -55,12 +62,21 @@ jobs:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
uses: actions/cache@v5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: "3.x"
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Build
run: |

View File

@ -83,7 +83,7 @@ jobs:
- name: Docker pull
run: |
docker pull pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
docker pull ${{ matrix.qemu-arch && format('--platform=linux/{0}', matrix.qemu-arch)}} pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
- name: Docker build
run: |

View File

@ -61,7 +61,7 @@ jobs:
- { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# Intel
- { os: "macos-15-intel", python-version: "3.10" }
- { os: "macos-26-intel", python-version: "3.10" }
exclude:
- { os: "macos-latest", python-version: "3.10" }
@ -91,6 +91,14 @@ jobs:
- name: Build system information
run: python3 .github/workflows/system-info.py
- name: Cache libavif
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
id: cache-libavif
with:
path: ~/cache-libavif
key: ${{ runner.os }}-libavif-${{ hashFiles('depends/install_libavif.sh', 'depends/libavif-svt4.patch') }}
- name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
@ -99,13 +107,23 @@ jobs:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Cache libwebp
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v5
id: cache-libwebp
with:
path: ~/cache-libwebp
key: ${{ runner.os }}-libwebp-${{ hashFiles('depends/install_webp.sh') }}
- name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu')
run: |
.ci/install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
GHA_LIBAVIF_CACHE_HIT: ${{ steps.cache-libavif.outputs.cache-hit }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
GHA_LIBWEBP_CACHE_HIT: ${{ steps.cache-libwebp.outputs.cache-hit }}
- name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS')

View File

@ -96,7 +96,7 @@ else
FREETYPE_VERSION=2.14.1
fi
HARFBUZZ_VERSION=12.3.2
LIBPNG_VERSION=1.6.54
LIBPNG_VERSION=1.6.55
JPEGTURBO_VERSION=3.1.3
OPENJPEG_VERSION=2.5.4
XZ_VERSION=5.8.2

View File

@ -53,19 +53,19 @@ jobs:
include:
- name: "macOS 10.10 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "cp3{10,11}*"
macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
platform: macos
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64
build: "{cp314,pp3}*"
macosx_deployment_target: "10.15"
@ -104,7 +104,7 @@ jobs:
cibw_arch: arm64_iphonesimulator
- name: "iOS x86_64 simulator"
platform: ios
os: macos-15-intel
os: macos-26-intel
cibw_arch: x86_64_iphonesimulator
steps:
- uses: actions/checkout@v6
@ -250,7 +250,7 @@ jobs:
runs-on: ubuntu-latest
name: Count dists
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
pattern: dist-*
path: dist
@ -269,7 +269,7 @@ jobs:
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
pattern: dist-!(sdist)*
path: dist
@ -291,7 +291,7 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v7
- uses: actions/download-artifact@v8
with:
pattern: dist-*
path: dist

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14
rev: v0.15.4
hooks:
- id: ruff-check
args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.9.3
rev: 1.9.4
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: v21.1.8
rev: v22.1.0
hooks:
- id: clang-format
types: [c]
@ -38,6 +38,7 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: check-merge-conflict
@ -51,7 +52,7 @@ repos:
exclude: ^\.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.36.1
rev: 0.37.0
hooks:
- id: check-github-workflows
- id: check-readthedocs
@ -68,12 +69,12 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.12.1
rev: v2.16.2
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
rev: v0.25
hooks:
- id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -221,6 +221,11 @@ def test_rle8_eof(file_name: str, length: int) -> None:
im.load()
def test_rle_delta() -> None:
with Image.open("Tests/images/bmp/q/pal8rletrns.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8rletrns.png")
def test_unsupported_bmp_bitfields_layout() -> None:
fp = io.BytesIO(
o32(40) # header size

View File

@ -310,6 +310,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
assert reloaded.getpixel((0, 0)) == 255
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("RGB", size)
with pytest.raises(SystemError):
im.save(b, "GIF")
@pytest.mark.parametrize(
"path, mode",
(

View File

@ -37,6 +37,14 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = io.BytesIO()
im = Image.new("1", size)
with pytest.raises(ValueError):
im.save(b, "PCX")
def test_p_4_planes() -> None:
with Image.open("Tests/images/p_4_planes.pcx") as im:
assert im.getpixel((0, 0)) == 3

View File

@ -68,6 +68,14 @@ def test_save(tmp_path: Path) -> None:
assert im2.format == "SPIDER"
@pytest.mark.parametrize("size", ((0, 1), (1, 0), (0, 0)))
def test_save_zero(size: tuple[int, int]) -> None:
b = BytesIO()
im = Image.new("1", size)
with pytest.raises(SystemError):
im.save(b, "SPIDER")
def test_tempfile() -> None:
# Arrange
im = hopper()

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from io import BytesIO
import io
from pathlib import Path
import pytest
@ -23,6 +23,13 @@ def test_reload() -> None:
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
def test_save_fp() -> None:
palette = ImagePalette.ImagePalette()
with io.StringIO() as fp:
palette.save(fp)
assert not fp.closed
def test_getcolor() -> None:
palette = ImagePalette.ImagePalette()
assert len(palette.palette) == 0
@ -204,7 +211,7 @@ def test_2bit_palette(tmp_path: Path) -> None:
def test_getpalette() -> None:
b = BytesIO(b"0 1\n1 2 3 4")
b = io.BytesIO(b"0 1\n1 2 3 4")
p = PaletteFile.PaletteFile(b)
palette, rawmode = p.getpalette()
@ -216,6 +223,6 @@ def test_invalid_palette() -> None:
with pytest.raises(OSError):
ImagePalette.load("Tests/images/hopper.jpg")
b = BytesIO(b"1" * 101)
b = io.BytesIO(b"1" * 101)
with pytest.raises(SyntaxError, match="bad palette file"):
PaletteFile.PaletteFile(b)

View File

@ -3,66 +3,88 @@ set -eo pipefail
version=1.3.0
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
if [[ "$GHA_LIBAVIF_CACHE_HIT" == "true" ]]; then
pushd libavif-$version
LIBDIR=/usr/lib/x86_64-linux-gnu
# Apply patch for SVT-AV1 4.0 compatibility
# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971
patch -p1 < ../libavif-svt4.patch
# Copy cached files into place
sudo cp ~/cache-libavif/lib/* $LIBDIR/
sudo cp -r ~/cache-libavif/include/avif /usr/include/
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
pushd libavif-$version
# Apply patch for SVT-AV1 4.0 compatibility
# Pending release of https://github.com/AOMediaCodec/libavif/pull/2971
patch -p1 < ../libavif-svt4.patch
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
PREFIX=$(brew --prefix)
else
PREFIX=/usr
fi
PKGCONFIG=${PKGCONFIG:-pkg-config}
LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi
cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MACOSX_RPATH=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
"${LIBAVIF_CMAKE_FLAGS[@]}" \
.
sudo make install
if [ -n "$GITHUB_ACTIONS" ] && [ "$(uname)" != "Darwin" ]; then
# Copy to cache
LIBDIR=/usr/lib/x86_64-linux-gnu
rm -rf ~/cache-libavif
mkdir -p ~/cache-libavif/lib
mkdir -p ~/cache-libavif/include
cp $LIBDIR/libavif.so* ~/cache-libavif/lib/
cp -r /usr/include/avif ~/cache-libavif/include/
fi
popd
fi
PKGCONFIG=${PKGCONFIG:-pkg-config}
LIBAVIF_CMAKE_FLAGS=()
HAS_DECODER=0
HAS_ENCODER=0
if $PKGCONFIG --exists aom; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
HAS_ENCODER=1
HAS_DECODER=1
fi
if $PKGCONFIG --exists dav1d; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists libgav1; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
HAS_DECODER=1
fi
if $PKGCONFIG --exists rav1e; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
HAS_ENCODER=1
fi
if $PKGCONFIG --exists SvtAv1Enc; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
HAS_ENCODER=1
fi
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
fi
cmake \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_MACOSX_RPATH=OFF \
-DAVIF_LIBSHARPYUV=LOCAL \
-DAVIF_LIBYUV=LOCAL \
"${LIBAVIF_CMAKE_FLAGS[@]}" \
.
make install
popd

View File

@ -3,10 +3,30 @@
archive=libwebp-1.6.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
if [[ "$GHA_LIBWEBP_CACHE_HIT" == "true" ]]; then
pushd $archive
# Copy cached files into place
sudo cp ~/cache-libwebp/lib/* /usr/lib/
sudo cp -r ~/cache-libwebp/include/webp /usr/include/
./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install
else
popd
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install
if [ -n "$GITHUB_ACTIONS" ]; then
# Copy to cache
rm -rf ~/cache-libwebp
mkdir -p ~/cache-libwebp/lib
mkdir -p ~/cache-libwebp/include
cp /usr/lib/libwebp*.so* /usr/lib/libwebp*.a ~/cache-libwebp/lib/
cp /usr/lib/libsharpyuv.so* /usr/lib/libsharpyuv.a ~/cache-libwebp/lib/
cp -r /usr/include/webp ~/cache-libwebp/include/
fi
popd
fi

View File

@ -828,16 +828,6 @@ PCX
Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.
PFM
^^^
.. versionadded:: 10.3.0
Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files
containing ``F`` data.
Color (PF format) PFM files are not supported.
Opening
~~~~~~~
@ -1081,12 +1071,19 @@ following parameters can also be set:
PPM
^^^
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
``RGB`` data.
Pillow reads and writes PBM, PGM, PPM, PNM and PFM files containing ``1``, ``L``, ``I``,
``RGB`` or ``F`` data.
"Raw" (P4 to P6) formats can be read, and are used when writing.
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
.. versionadded:: 9.2.0
"Plain" (P1 to P3) formats can be read.
.. versionadded:: 10.3.0
Grayscale (Pf format) Portable FloatMap (PFM) files containing
``F`` data can be read and used when writing.
Color (PF format) PFM files are not supported.
QOI
^^^

View File

@ -39,15 +39,15 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 15 Sequoia | 3.10 | x86-64 |
| +----------------------------+---------------------+
| | 3.11, 3.12, 3.13, 3.14, | arm64 |
| | PyPy3 | |
| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 |
| | 3.15, PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| macOS 26 Tahoe | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 |
| | 3.14, PyPy3 | |
| | 3.14, 3.15, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
@ -55,7 +55,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.10 | x86 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 |
| | PyPy3 | |
| | 3.15, PyPy3 | |
| +----------------------------+---------------------+
| | 3.13 (MinGW) | x86-64 |
+----------------------------------+----------------------------+---------------------+

View File

@ -62,7 +62,6 @@ optional-dependencies.test-arrow = [
"nanoarrow",
"pyarrow",
]
optional-dependencies.tests = [
"check-manifest",
"coverage>=7.4.2",
@ -77,16 +76,15 @@ optional-dependencies.tests = [
"pytest-xdist",
"trove-classifiers>=2024.10.12",
]
optional-dependencies.xmp = [
"defusedxml",
]
urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
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.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"
[tool.setuptools]
@ -95,70 +93,50 @@ packages = [
]
include-package-data = true
package-dir = { "" = "src" }
[tool.setuptools.dynamic]
version = { attr = "PIL.__version__" }
dynamic.version = { attr = "PIL.__version__" }
[tool.cibuildwheel]
before-all = ".github/workflows/wheels-dependencies.sh"
build-verbosity = 1
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"
test-requires = [
"numpy",
]
xbuild-tools = [ ]
[tool.cibuildwheel.ios]
xbuild-tools = []
# Disable platform guessing on iOS, and disable raqm (since there won't be a
# vendor version, and we can't distribute it due to licensing)
config-settings = "raqm=disable imagequant=disable platform-guessing=disable"
ios.config-settings = "raqm=disable imagequant=disable platform-guessing=disable"
# iOS needs to be given a specific pytest invocation and list of test sources.
test-sources = [
ios.test-sources = [
"checks",
"Tests",
"selftest.py",
]
test-command = [
ios.test-command = [
"python -m selftest",
"python -m pytest -vv -x -W always checks/check_wheel.py Tests",
]
# There's no numpy wheel for iOS (yet...)
test-requires = [ ]
[tool.cibuildwheel.macos]
ios.test-requires = []
# Disable platform guessing on macOS to avoid picking up Homebrew etc.
config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
[tool.cibuildwheel.macos.environment]
macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable"
# Isolate macOS build environment from Homebrew etc.
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
[[tool.cibuildwheel.overrides]]
# iOS environment is isolated by cibuildwheel, but needs the dependencies
select = "*_iphoneos"
environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH"
[[tool.cibuildwheel.overrides]]
# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies
select = "*_iphonesimulator"
environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH"
[[tool.cibuildwheel.overrides]]
select = "*-win32"
test-requires = [ ]
macos.environment.PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
overrides = [
# iOS environment is isolated by cibuildwheel, but needs the dependencies
{ select = "*_iphoneos", environment.PATH = "$(pwd)/build/deps/iphoneos/bin:$PATH" },
# iOS simulator environment is isolated by cibuildwheel, but needs the dependencies
{ select = "*_iphonesimulator", environment.PATH = "$(pwd)/build/deps/iphonesimulator/bin:$PATH" },
{ select = "*-win32", test-requires = [] },
]
[tool.black]
exclude = "wheels/multibuild"
[tool.ruff]
exclude = [ "wheels/multibuild" ]
fix = true
lint.select = [
"C4", # flake8-comprehensions
@ -207,8 +185,8 @@ lint.isort.required-imports = [
[tool.pyproject-fmt]
max_supported_python = "3.14"
[tool.pytest.ini_options]
addopts = "-ra --color=auto"
[tool.pytest]
addopts = [ "-ra", "--color=auto" ]
testpaths = [
"Tests",
]

View File

@ -369,7 +369,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
bytes_read = self.fd.read(2)
if len(bytes_read) < 2:
break
right, up = self.fd.read(2)
right, up = bytes_read
data += b"\x00" * (right + up * self.state.xsize)
x = len(data) % self.state.xsize
else:

View File

@ -937,7 +937,13 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
:param info: encoderinfo
:returns: list of indexes of palette entries in use, or None
"""
if im.mode in ("P", "L") and info and info.get("optimize"):
if (
im.mode in ("P", "L")
and info
and info.get("optimize")
and im.width != 0
and im.height != 0
):
# Potentially expensive operation.
# The palette saves 3 bytes per color not used, but palette

View File

@ -579,10 +579,7 @@ class Parser:
pass # not enough data
else:
flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
if flag or len(im.tile) != 1:
# custom load code, or multiple tiles
self.decode = None
else:
if not flag and len(im.tile) == 1:
# initialize decoder
im.load_prepare()
d, e, o, a = im.tile[0]

View File

@ -191,19 +191,24 @@ class ImagePalette:
if self.rawmode:
msg = "palette contains raw palette data"
raise ValueError(msg)
open_fp = False
if isinstance(fp, str):
fp = open(fp, "w")
fp.write("# Palette\n")
fp.write(f"# Mode: {self.mode}\n")
for i in range(256):
fp.write(f"{i}")
for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
try:
fp.write(f" {self.palette[j]}")
except IndexError:
fp.write(" 0")
fp.write("\n")
fp.close()
open_fp = True
try:
fp.write("# Palette\n")
fp.write(f"# Mode: {self.mode}\n")
for i in range(256):
fp.write(f"{i}")
for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
try:
fp.write(f" {self.palette[j]}")
except IndexError:
fp.write(" 0")
fp.write("\n")
finally:
if open_fp:
fp.close()
# --------------------------------------------------------------------

View File

@ -59,11 +59,10 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ b"MPF\0"
+ b" " * ifd_length
)
exif = im_frame.encoderinfo.get("exif")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
im_frame.encoderinfo["exif"] = exif
if exif:
if exif := im_frame.encoderinfo.get("exif"):
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
im_frame.encoderinfo["exif"] = exif
mpf_offset += 4 + len(exif)
JpegImagePlugin._save(im_frame, fp, filename)

View File

@ -146,6 +146,10 @@ SAVE = {
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.width == 0 or im.height == 0:
msg = "Cannot write empty image as PCX"
raise ValueError(msg)
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:

View File

@ -1403,8 +1403,7 @@ def _save(
chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
if icc:
if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")):
# ICC profile
# according to PNG spec, the iCCP chunk contains:
# Profile name 1-79 bytes (character string)
@ -1419,8 +1418,7 @@ def _save(
# Disallow sRGB chunks when an iCCP-chunk has been emitted.
chunks.remove(b"sRGB")
info = im.encoderinfo.get("pnginfo")
if info:
if info := im.encoderinfo.get("pnginfo"):
chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
for info_chunk in info.chunks:
cid, data = info_chunk[:2]
@ -1472,8 +1470,7 @@ def _save(
alpha_bytes = colors
chunk(fp, b"tRNS", alpha[:alpha_bytes])
dpi = im.encoderinfo.get("dpi")
if dpi:
if dpi := im.encoderinfo.get("dpi"):
chunk(
fp,
b"pHYs",
@ -1490,8 +1487,7 @@ def _save(
chunks.remove(cid)
chunk(fp, cid, data)
exif = im.encoderinfo.get("exif")
if exif:
if exif := im.encoderinfo.get("exif"):
if isinstance(exif, Image.Exif):
exif = exif.tobytes(8)
if exif.startswith(b"Exif\x00\x00"):

View File

@ -244,7 +244,7 @@ def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | No
def makeSpiderHeader(im: Image.Image) -> list[bytes]:
nsam, nrow = im.size
lenbyt = nsam * 4 # There are labrec records in the header
lenbyt = max(1, nsam) * 4 # There are labrec records in the header
labrec = int(1024 / lenbyt)
if 1024 % lenbyt != 0:
labrec += 1

View File

@ -124,8 +124,10 @@ PyImagingPhotoPut(
if (im->mode == IMAGING_MODE_1 || im->mode == IMAGING_MODE_L) {
block.pixelSize = 1;
block.offset[0] = block.offset[1] = block.offset[2] = block.offset[3] = 0;
} else if (im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa) {
} else if (
im->mode == IMAGING_MODE_RGB || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_RGBX || im->mode == IMAGING_MODE_RGBa
) {
block.pixelSize = 4;
block.offset[0] = 0;
block.offset[1] = 1;

View File

@ -485,7 +485,7 @@ _encoder_add(AvifEncoderObject *self, PyObject *args) {
frame = image;
} else {
frame = avifImageCreateEmpty();
if (image == NULL) {
if (frame == NULL) {
PyErr_SetString(PyExc_ValueError, "Image creation failed");
return NULL;
}

View File

@ -999,8 +999,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, PyBytes_AsString(value)
);
} else if (type == TIFF_DOUBLE || type == TIFF_SRATIONAL ||
type == TIFF_RATIONAL) {
} else if (
type == TIFF_DOUBLE || type == TIFF_SRATIONAL || type == TIFF_RATIONAL
) {
status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value)
);

View File

@ -170,16 +170,17 @@ export_named_type(struct ArrowSchema *schema, char *format, const char *name) {
strncpy(formatp, format, format_len);
strncpy(namep, name, name_len);
*schema = (struct ArrowSchema){// Type description
.format = formatp,
.name = namep,
.metadata = NULL,
.flags = 0,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &ReleaseExportedSchema
*schema = (struct ArrowSchema){
// Type description
.format = formatp,
.name = namep,
.metadata = NULL,
.flags = 0,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &ReleaseExportedSchema
};
return 0;
}
@ -287,17 +288,18 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) {
im->refcount++;
MUTEX_UNLOCK(&im->mutex);
// Initialize primitive fields
*array = (struct ArrowArray){// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
*array = (struct ArrowArray){
// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
};
// Allocate list of buffers
@ -332,17 +334,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
// Initialize primitive fields
// Fixed length arrays are 1 buffer of validity, and the length in pixels.
// Data is in a child array.
*array = (struct ArrowArray){// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 1,
.n_children = 1,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
*array = (struct ArrowArray){
// Data description
.length = length,
.offset = 0,
.null_count = 0,
.n_buffers = 1,
.n_children = 1,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
};
// Allocate list of buffers
@ -367,17 +370,18 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
MUTEX_LOCK(&im->mutex);
im->refcount++;
MUTEX_UNLOCK(&im->mutex);
*array->children[0] = (struct ArrowArray){// Data description
.length = length * 4,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
*array->children[0] = (struct ArrowArray){
// Data description
.length = length * 4,
.offset = 0,
.null_count = 0,
.n_buffers = 2,
.n_children = 0,
.children = NULL,
.dictionary = NULL,
// Bookkeeping
.release = &release_const_array,
.private_data = im
};
array->children[0]->buffers =

View File

@ -1695,16 +1695,20 @@ ImagingConvertTransparent(Imaging imIn, const ModeID mode, int r, int g, int b)
if (mode == IMAGING_MODE_RGBa) {
premultiplied = 1;
}
} else if (imIn->mode == IMAGING_MODE_RGB &&
(mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)) {
} else if (
imIn->mode == IMAGING_MODE_RGB &&
(mode == IMAGING_MODE_LA || mode == IMAGING_MODE_La)
) {
convert = rgb2la;
source_transparency = 1;
if (mode == IMAGING_MODE_La) {
premultiplied = 1;
}
} else if ((imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I ||
imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) &&
(mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)) {
} else if (
(imIn->mode == IMAGING_MODE_1 || imIn->mode == IMAGING_MODE_I ||
imIn->mode == IMAGING_MODE_I_16 || imIn->mode == IMAGING_MODE_L) &&
(mode == IMAGING_MODE_RGBA || mode == IMAGING_MODE_LA)
) {
if (imIn->mode == IMAGING_MODE_1) {
convert = bit2rgb;
} else if (imIn->mode == IMAGING_MODE_I) {

View File

@ -537,8 +537,9 @@ polygon_generic(
// Needed to draw consistent polygons
xx[j] = xx[j - 1];
j++;
} else if ((ymin == current->ymin || ymin == current->ymax) &&
current->dx != 0) {
} else if (
(ymin == current->ymin || ymin == current->ymax) && current->dx != 0
) {
// Connect discontiguous corners
for (k = 0; k < i; k++) {
Edge *other_edge = edge_table[k];
@ -570,8 +571,10 @@ polygon_generic(
adjacent_line_x, adjacent_line_x_other_edge
)) +
1;
} else if (xx[j - 1] < adjacent_line_x - 1 &&
xx[j - 1] < adjacent_line_x_other_edge - 1) {
} else if (
xx[j - 1] < adjacent_line_x - 1 &&
xx[j - 1] < adjacent_line_x_other_edge - 1
) {
xx[j - 1] =
roundf(fmin(
adjacent_line_x, adjacent_line_x_other_edge

View File

@ -89,10 +89,12 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
INT32 mask = 0xffffffff;
if (im->bands == 3) {
((UINT8 *)&mask)[3] = 0;
} else if (alpha_only &&
(im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA ||
im->mode == IMAGING_MODE_PA)) {
} else if (
alpha_only &&
(im->mode == IMAGING_MODE_RGBa || im->mode == IMAGING_MODE_RGBA ||
im->mode == IMAGING_MODE_La || im->mode == IMAGING_MODE_LA ||
im->mode == IMAGING_MODE_PA)
) {
#ifdef WORDS_BIGENDIAN
mask = 0x000000ff;
#else

View File

@ -330,8 +330,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
components = 4;
color_space = OPJ_CLRSPC_SRGB;
pack = j2k_pack_rgba;
#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2)
#if ( \
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2 \
)
} else if (im->mode == IMAGING_MODE_CMYK) {
components = 4;
color_space = OPJ_CLRSPC_CMYK;

View File

@ -206,8 +206,10 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by
context->cinfo.out_color_space = JCS_EXT_RGBX;
}
#endif
else if (context->rawmode == IMAGING_RAWMODE_CMYK ||
context->rawmode == IMAGING_RAWMODE_CMYK_I) {
else if (
context->rawmode == IMAGING_RAWMODE_CMYK ||
context->rawmode == IMAGING_RAWMODE_CMYK_I
) {
context->cinfo.out_color_space = JCS_CMYK;
} else if (context->rawmode == IMAGING_RAWMODE_YCbCr) {
context->cinfo.out_color_space = JCS_YCbCr;

View File

@ -46,8 +46,9 @@ ImagingConvertMatrix(Imaging im, const ModeID mode, float m[]) {
}
}
ImagingSectionLeave(&cookie);
} else if (mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB ||
mode == IMAGING_MODE_RGB) {
} else if (
mode == IMAGING_MODE_HSV || mode == IMAGING_MODE_LAB || mode == IMAGING_MODE_RGB
) {
imOut = ImagingNewDirty(mode, im->xsize, im->ysize);
if (!imOut) {
return NULL;

View File

@ -121,7 +121,7 @@ V = {
"LCMS2": "2.18",
"LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.4.1",
"LIBPNG": "1.6.54",
"LIBPNG": "1.6.55",
"LIBWEBP": "1.6.0",
"OPENJPEG": "2.5.4",
"TIFF": "4.7.1",