Merge branch 'main' into reduce-contention

This commit is contained in:
Andrew Murray 2025-03-17 13:29:09 +11:00 committed by GitHub
commit f91fcf34e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
117 changed files with 1208 additions and 866 deletions

View File

@ -2,12 +2,12 @@
aptget_update()
{
if [ ! -z $1 ]; then
if [ -n "$1" ]; then
echo ""
echo "Retrying apt-get update..."
echo ""
fi
output=`sudo apt-get update 2>&1`
output=$(sudo apt-get update 2>&1)
echo "$output"
if [[ $output == *[WE]:\ * ]]; then
return 1
@ -20,7 +20,7 @@ fi
set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev

View File

@ -1 +1 @@
cibuildwheel==2.22.0
cibuildwheel==2.23.1

View File

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

View File

@ -16,6 +16,6 @@
}
],
"schedule": [
"on the 3rd day of the month"
"* * 3 * *"
]
}

View File

@ -35,6 +35,10 @@ jobs:
matrix:
os: ["ubuntu-latest"]
docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-24.04-noble-ppc64le,
ubuntu-24.04-noble-s390x,
# Then run the remainder
alpine,
amazon-2-amd64,
amazon-2023-amd64,
@ -52,13 +56,9 @@ jobs:
dockerTag: [main]
include:
- docker: "ubuntu-24.04-noble-ppc64le"
os: "ubuntu-22.04"
qemu-arch: "ppc64le"
dockerTag: main
- docker: "ubuntu-24.04-noble-s390x"
os: "ubuntu-22.04"
qemu-arch: "s390x"
dockerTag: main
- docker: "ubuntu-24.04-noble-arm64v8"
os: "ubuntu-24.04-arm"
dockerTag: main
@ -75,8 +75,9 @@ jobs:
- name: Set up QEMU
if: "matrix.qemu-arch"
run: |
docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }}
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ matrix.qemu-arch }}
- name: Docker pull
run: |

View File

@ -60,6 +60,7 @@ jobs:
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-ghostscript \
mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libtiff \

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
architecture: ["x64"]
os: ["windows-latest"]
include:
@ -94,8 +94,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.4.0 --no-progress
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.5.0 --no-progress
echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -41,6 +41,7 @@ jobs:
"ubuntu-latest",
]
python-version: [
"pypy3.11",
"pypy3.10",
"3.14",
"3.13t",

View File

@ -38,14 +38,14 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.2.0
LIBPNG_VERSION=1.6.46
HARFBUZZ_VERSION=10.4.0
LIBPNG_VERSION=1.6.47
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.4
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
ZLIB_NG_VERSION=2.2.3
TIFF_VERSION=4.7.0
LCMS2_VERSION=2.17
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
@ -54,13 +54,10 @@ 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 \
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
}
@ -72,6 +69,14 @@ function build_zlib_ng {
&& ./configure --prefix=$BUILD_PREFIX --zlib-compat \
&& make -j4 \
&& make install)
if [ -n "$IS_MACOS" ]; then
# Ensure that on macOS, the library name is an absolute path, not an
# @rpath, so that delocate picks up the right library (and doesn't need
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
# option to control the install_name.
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
fi
touch zlib-stamp
}
@ -130,15 +135,13 @@ function build {
build_lcms2
build_openjpeg
ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG"
webp_cflags="-O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
fi
build_simple libwebp $LIBWEBP_VERSION \
CFLAGS="$CFLAGS $webp_cflags" 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

View File

@ -63,7 +63,7 @@ jobs:
- name: "macOS 10.15 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310*"
build: "pp3*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
os: macos-latest

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.4
rev: v0.9.9
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@ -11,7 +11,7 @@ repos:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.8.2
rev: 1.8.3
hooks:
- id: bandit
args: [--severity-level=high]
@ -50,14 +50,14 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.31.1
rev: 0.31.2
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.3.0
rev: v1.4.1
hooks:
- id: zizmor
@ -67,7 +67,7 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.0
rev: v2.5.1
hooks:
- id: pyproject-fmt

View File

@ -9,7 +9,6 @@ import os
import shutil
import subprocess
import sys
import sysconfig
import tempfile
from collections.abc import Sequence
from functools import lru_cache
@ -342,10 +341,6 @@ def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info")
def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw"
class CachedProperty:
def __init__(self, func: Callable[[Any], Any]) -> None:
self.func = func

View File

@ -34,8 +34,11 @@ def test_apng_basic() -> None:
with pytest.raises(EOFError):
im.seek(2)
# test rewind support
im.seek(0)
with pytest.raises(ValueError, match="cannot seek to frame 2"):
im._seek(2)
# test rewind support
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
assert im.getpixel((64, 32)) == (255, 0, 0, 255)
im.seek(1)

View File

@ -26,12 +26,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_FILE)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -331,11 +331,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None:
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
px = im.getpixel((0, 0))
assert isinstance(px, tuple)
assert px[0] != 0
assert px[1] != 0
assert px[2] != 0
px = im.getpixel((1, 0))
assert isinstance(px, tuple)
assert px[0] != 0
assert px[1] != 0
assert px[2] != 0

View File

@ -95,10 +95,14 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_load() -> None:
with Image.open(FILE1) as im:
assert im.load()[0, 0] == (255, 255, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (255, 255, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
def test_binary() -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import io
import warnings
import pytest
@ -52,12 +53,12 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(static_test_file)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:
@ -132,6 +133,15 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_missing_frame_size() -> None:
with open(animated_test_file, "rb") as fp:
data = fp.read()
data = data[:6188]
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(EOFError, match="missing frame size"):
im.seek(1)
def test_seek_tell() -> None:
with Image.open(animated_test_file) as im:
layer_number = im.tell()
@ -160,6 +170,9 @@ def test_seek() -> None:
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
with pytest.raises(ValueError, match="cannot seek to frame 52"):
im._seek(52)
@pytest.mark.parametrize(
"test_file",

View File

@ -1,5 +1,8 @@
from __future__ import annotations
import io
import struct
import pytest
from PIL import FtexImagePlugin, Image
@ -23,3 +26,15 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError):
FtexImagePlugin.FtexImageFile(invalid_file)
def test_invalid_texture() -> None:
with open("Tests/images/ftex_dxt1.ftc", "rb") as fp:
data = fp.read()
# Change texture compression format
data = data[:24] + struct.pack("<i", 2) + data[28:]
with pytest.raises(ValueError, match="Invalid texture compression format: 2"):
with Image.open(io.BytesIO(data)):
pass

View File

@ -14,10 +14,14 @@ def test_gbr_file() -> None:
def test_load() -> None:
with Image.open("Tests/images/gbr.gbr") as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
def test_multiple_load_operations() -> None:

View File

@ -4,6 +4,8 @@ import pytest
from PIL import GdImageFile, UnidentifiedImageError
from .helper import assert_image_similar_tofile
TEST_GD_FILE = "Tests/images/hopper.gd"
@ -11,6 +13,7 @@ def test_sanity() -> None:
with GdImageFile.open(TEST_GD_FILE) as im:
assert im.size == (128, 128)
assert im.format == "GD"
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
def test_bad_mode() -> None:

View File

@ -22,9 +22,6 @@ from .helper import (
# sample gif stream
TEST_GIF = "Tests/images/hopper.gif"
with open(TEST_GIF, "rb") as f:
data = f.read()
def test_sanity() -> None:
with Image.open(TEST_GIF) as im:
@ -37,12 +34,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_GIF)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:
@ -310,6 +307,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
def test_loading_multiple_palettes(path: str, mode: str) -> None:
with Image.open(path) as im:
assert im.mode == "P"
assert im.palette is not None
first_frame_colors = im.palette.colors.keys()
original_color = im.convert("RGB").getpixel((0, 0))
@ -412,6 +410,10 @@ def test_seek() -> None:
except EOFError:
assert frame_count == 5
img.seek(0)
with pytest.raises(ValueError, match="cannot seek to frame 2"):
img._seek(2)
def test_seek_info() -> None:
with Image.open("Tests/images/iss634.gif") as im:
@ -528,6 +530,7 @@ def test_dispose_background_transparency() -> None:
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
img.seek(2)
px = img.load()
assert px is not None
assert px[35, 30][3] == 0
@ -602,7 +605,7 @@ def test_save_dispose(tmp_path: Path) -> None:
Image.new("L", (100, 100), "#111"),
Image.new("L", (100, 100), "#222"),
]
for method in range(0, 4):
for method in range(4):
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
with Image.open(out) as img:
for _ in range(2):
@ -762,6 +765,21 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
assert im.getpixel((0, 0)) == (0, 0, 0, 255)
def test_dispose2_without_transparency(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("P", (100, 100))
im2 = Image.new("P", (100, 100), (0, 0, 0))
im2.putpixel((50, 50), (255, 0, 0))
im.save(out, save_all=True, append_images=[im2], disposal=2)
with Image.open(out) as reloaded:
reloaded.seek(1)
assert reloaded.tile[0].extents == (0, 0, 100, 100)
def test_transparency_in_second_frame(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
@ -1311,6 +1329,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
with Image.open(out) as im:
# Assert that the frames are correct, and each frame has the same palette
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
assert im.palette is not None
assert im.palette.palette == im.global_palette.palette
im.seek(1)
@ -1345,32 +1364,30 @@ def test_save_I(tmp_path: Path) -> None:
assert_image_equal(reloaded.convert("L"), im.convert("L"))
def test_getdata() -> None:
def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
# Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
im = Image.linear_gradient(mode="L").resize((16, 16), Image.Resampling.NEAREST)
im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0}
passed_palette = bytes(255 - i // 3 for i in range(768))
GifImagePlugin._FORCE_OPTIMIZE = True
try:
h = GifImagePlugin.getheader(im, passed_palette)
d = GifImagePlugin.getdata(im)
monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True)
import pickle
h = GifImagePlugin.getheader(im, passed_palette)
d = GifImagePlugin.getdata(im)
# Enable to get target values on pre-refactor version
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
# pickle.dump((h, d), f, 1)
with open("Tests/images/gif_header_data.pkl", "rb") as f:
(h_target, d_target) = pickle.load(f)
import pickle
assert h == h_target
assert d == d_target
finally:
GifImagePlugin._FORCE_OPTIMIZE = False
# Enable to get target values on pre-refactor version
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
# pickle.dump((h, d), f, 1)
with open("Tests/images/gif_header_data.pkl", "rb") as f:
(h_target, d_target) = pickle.load(f)
assert h == h_target
assert d == d_target
def test_lzw_bits() -> None:

View File

@ -32,10 +32,14 @@ def test_sanity() -> None:
def test_load() -> None:
with Image.open(TEST_FILE) as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0)
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0, 0)
def test_save(tmp_path: Path) -> None:

View File

@ -24,7 +24,9 @@ def test_sanity() -> None:
def test_load() -> None:
with Image.open(TEST_ICO_FILE) as im:
assert im.load()[0, 0] == (1, 1, 9, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (1, 1, 9, 255)
def test_mask() -> None:

View File

@ -31,12 +31,12 @@ def test_name_limit(tmp_path: Path) -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_IM)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -63,6 +63,7 @@ def test_sanity() -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load()
assert px is not None
assert px[0, 0] == (0, 0, 0)
assert im.mode == "RGB"
assert im.size == (640, 480)
@ -312,6 +313,18 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA"
def test_grayscale_four_channels() -> None:
with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp:
data = fp.read()
# Change color space to OPJ_CLRSPC_GRAY
data = data[:76] + b"\x11" + data[77:]
with Image.open(BytesIO(data)) as im:
im.load()
assert im.mode == "RGBA"
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@ -421,6 +434,7 @@ def test_subsampling_decode(name: str) -> None:
def test_pclr() -> None:
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
assert im.mode == "P"
assert im.palette is not None
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
@ -428,6 +442,7 @@ def test_pclr() -> None:
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert im.palette is not None
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0

View File

@ -1140,11 +1140,9 @@ class TestFileLibTiff(LibTiffTestCase):
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e:
im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "decoder error -9"
with pytest.raises(OSError, match="decoder error -9"):
im.load()
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:

View File

@ -29,21 +29,26 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
@pytest.mark.parametrize("test_file", test_files)
def test_sanity(test_file: str) -> None:
with Image.open(test_file) as im:
def check(im: ImageFile.ImageFile) -> None:
im.load()
assert im.mode == "RGB"
assert im.size == (640, 480)
assert im.format == "MPO"
with Image.open(test_file) as im:
check(im)
with MpoImagePlugin.MpoImageFile(test_file) as im:
check(im)
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(test_files[0])
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:
@ -77,8 +82,8 @@ def test_app(test_file: str) -> None:
with Image.open(test_file) as im:
assert im.applist[0][0] == "APP1"
assert im.applist[1][0] == "APP2"
assert (
im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
assert im.applist[1][1].startswith(
b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
)
assert len(im.applist) == 2

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import io
from pathlib import Path
import pytest
@ -36,6 +37,28 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
def test_bad_image_size() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:4] + b"\xff\xff" + data[6:]
b = io.BytesIO(data)
with pytest.raises(SyntaxError, match="bad PCX image size"):
with PcxImagePlugin.PcxImageFile(b):
pass
def test_unknown_mode() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
data = data[:3] + b"\xff" + data[4:]
b = io.BytesIO(data)
with pytest.raises(OSError, match="unknown PCX mode"):
with Image.open(b):
pass
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"

View File

@ -79,6 +79,7 @@ def test_arbitrary_maxval(
assert im.mode == mode
px = im.load()
assert px is not None
assert tuple(px[x, 0] for x in range(3)) == pixels
@ -292,12 +293,10 @@ def test_header_token_too_long(tmp_path: Path) -> None:
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
with Image.open(path):
pass
assert str(e.value) == "Token too long in file header: 01234567890"
def test_truncated_file(tmp_path: Path) -> None:
# Test EOF in header
@ -305,12 +304,10 @@ def test_truncated_file(tmp_path: Path) -> None:
with open(path, "wb") as f:
f.write(b"P6")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Reached EOF while reading header"):
with Image.open(path):
pass
assert str(e.value) == "Reached EOF while reading header"
# Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im:
@ -334,12 +331,12 @@ def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval)
with pytest.raises(ValueError) as e:
with pytest.raises(
ValueError, match="maxval must be greater than 0 and less than 65536"
):
with Image.open(path):
pass
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
def test_neg_ppm() -> None:
# Storage.c accepted negative values for xsize, ysize. the

View File

@ -25,12 +25,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(test_file)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -24,12 +24,12 @@ def test_sanity() -> None:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open(TEST_FILE)
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file() -> None:

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import io
import os
import pytest
from PIL import Image, SunImagePlugin
from PIL import Image, SunImagePlugin, _binary
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -33,6 +34,60 @@ def test_im1() -> None:
assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png")
def _sun_header(
depth: int = 0, file_type: int = 0, palette_length: int = 0
) -> io.BytesIO:
return io.BytesIO(
_binary.o32be(0x59A66A95)
+ b"\x00" * 8
+ _binary.o32be(depth)
+ b"\x00" * 4
+ _binary.o32be(file_type)
+ b"\x00" * 4
+ _binary.o32be(palette_length)
)
def test_unsupported_mode_bit_depth() -> None:
with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"):
with SunImagePlugin.SunImageFile(_sun_header()):
pass
def test_unsupported_color_palette_length() -> None:
with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)):
pass
def test_unsupported_palette_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Palette Type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)):
pass
def test_unsupported_file_type() -> None:
with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"):
with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)):
pass
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_rgbx() -> None:
with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp:
data = fp.read()
# Set file type to 3
data = data[:20] + _binary.o32be(3) + data[24:]
with Image.open(io.BytesIO(data)) as im:
r, g, b = im.split()
im = Image.merge("RGB", (b, g, r))
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import warnings
from pathlib import Path
import pytest
@ -29,6 +30,22 @@ def test_sanity(codec: str, test_path: str, format: str) -> None:
assert im.format == format
def test_unexpected_end(tmp_path: Path) -> None:
tmpfile = str(tmp_path / "temp.tar")
with open(tmpfile, "w"):
pass
with pytest.raises(OSError, match="unexpected end of tar file"):
with TarIO.TarIO(tmpfile, "test"):
pass
def test_cannot_find_subfile() -> None:
with pytest.raises(OSError, match="cannot find subfile"):
with TarIO.TarIO(TEST_TAR_FILE, "test"):
pass
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
with pytest.warns(ResourceWarning):

View File

@ -72,6 +72,7 @@ def test_palette_depth_8(tmp_path: Path) -> None:
def test_palette_depth_16(tmp_path: Path) -> None:
with Image.open("Tests/images/p_16.tga") as im:
assert im.palette is not None
assert im.palette.mode == "RGBA"
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
@ -213,10 +214,14 @@ def test_save_orientation(tmp_path: Path) -> None:
def test_horizontal_orientations() -> None:
# These images have been manually hexedited to have the relevant orientations
with Image.open("Tests/images/rgb32rle_top_right.tga") as im:
assert im.load()[90, 90][:3] == (0, 0, 0)
px = im.load()
assert px is not None
assert px[90, 90][:3] == (0, 0, 0)
with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im:
assert im.load()[90, 90][:3] == (0, 255, 0)
px = im.load()
assert px is not None
assert px[90, 90][:3] == (0, 255, 0)
def test_save_rle(tmp_path: Path) -> None:

View File

@ -63,12 +63,12 @@ class TestFileTiff:
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file(self) -> None:
def open() -> None:
def open_test_image() -> None:
im = Image.open("Tests/images/multipage.tiff")
im.load()
with pytest.warns(ResourceWarning):
open()
open_test_image()
def test_closed_file(self) -> None:
with warnings.catch_warnings():
@ -134,9 +134,8 @@ class TestFileTiff:
def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="Not allowing setting of legacy api"):
ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None:
filename = "Tests/images/pil168.tif"
@ -661,6 +660,18 @@ class TestFileTiff:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 256
im = hopper()
im2 = Image.new("L", (128, 128))
im2.encoderinfo = {"tiffinfo": {278: 256}}
im.save(outfile, save_all=True, append_images=[im2])
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[278] == 128
im.seek(1)
assert im.tag_v2[278] == 256
def test_strip_raw(self) -> None:
infile = "Tests/images/tiff_strip_raw.tif"
with Image.open(infile) as im:

View File

@ -21,7 +21,11 @@ def test_open() -> None:
def test_load() -> None:
with WalImageFile.open(TEST_FILE) as im:
assert im.load()[0, 0] == 122
px = im.load()
assert px is not None
assert px[0, 0] == 122
# Test again now that it has already been loaded once
assert im.load()[0, 0] == 122
px = im.load()
assert px is not None
assert px[0, 0] == 122

View File

@ -154,9 +154,8 @@ class TestFileWebp:
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="encoding error 6"):
im.save(tmp_path / "temp.webp", method=0)
assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
@ -231,7 +230,7 @@ class TestFileWebp:
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3))
assert difference < 5
def test_duration(self, tmp_path: Path) -> None:

View File

@ -40,7 +40,7 @@ def test_read_exif_metadata() -> None:
def test_read_exif_metadata_without_prefix() -> None:
with Image.open("Tests/images/flower2.webp") as im:
# Assert prefix is not present
assert im.info["exif"][:6] != b"Exif\x00\x00"
assert not im.info["exif"].startswith(b"Exif\x00\x00")
exif = im.getexif()
assert exif[305] == "Adobe Photoshop CS6 (Macintosh)"

View File

@ -32,7 +32,9 @@ def test_load_raw() -> None:
def test_load() -> None:
with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"):
assert im.load()[0, 0] == (255, 255, 255)
px = im.load()
assert px is not None
assert px[0, 0] == (255, 255, 255)
def test_load_zero_inch() -> None:

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import io
import pytest
from PIL import BdfFontFile, FontFile
@ -8,13 +10,20 @@ filename = "Tests/images/courB08.bdf"
def test_sanity() -> None:
with open(filename, "rb") as test_file:
font = BdfFontFile.BdfFontFile(test_file)
with open(filename, "rb") as fp:
font = BdfFontFile.BdfFontFile(fp)
assert isinstance(font, FontFile.FontFile)
assert len([_f for _f in font.glyph if _f]) == 190
def test_zero_width_chars() -> None:
with open(filename, "rb") as fp:
data = fp.read()
data = data[:2650] + b"\x00\x00" + data[2652:]
BdfFontFile.BdfFontFile(io.BytesIO(data))
def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):

View File

@ -4,7 +4,20 @@ from pathlib import Path
import pytest
from PIL import FontFile
from PIL import FontFile, Image
def test_compile() -> None:
font = FontFile.FontFile()
font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0)))
font.compile()
assert font.ysize == 1
font.ysize = 2
font.compile()
# Assert that compiling again did not change anything
assert font.ysize == 2
def test_save(tmp_path: Path) -> None:

View File

@ -22,28 +22,26 @@ def test_sanity() -> None:
Image.new("HSV", (100, 100))
def wedge() -> Image.Image:
w = Image._wedge()
w90 = w.rotate(90)
def linear_gradient() -> Image.Image:
im = Image.linear_gradient(mode="L")
im90 = im.rotate(90)
(px, h) = w.size
(px, h) = im.size
r = Image.new("L", (px * 3, h))
g = r.copy()
b = r.copy()
r.paste(w, (0, 0))
r.paste(w90, (px, 0))
r.paste(im, (0, 0))
r.paste(im90, (px, 0))
g.paste(w90, (0, 0))
g.paste(w, (2 * px, 0))
g.paste(im90, (0, 0))
g.paste(im, (2 * px, 0))
b.paste(w, (px, 0))
b.paste(w90, (2 * px, 0))
b.paste(im, (px, 0))
b.paste(im90, (2 * px, 0))
img = Image.merge("RGB", (r, g, b))
return img
return Image.merge("RGB", (r, g, b))
def to_xxx_colorsys(
@ -79,8 +77,8 @@ def to_rgb_colorsys(im: Image.Image) -> Image.Image:
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
def test_wedge() -> None:
src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR)
def test_linear_gradient() -> None:
src = linear_gradient().resize((3 * 32, 32), Image.Resampling.BILINEAR)
im = src.convert("HSV")
comparable = to_hsv_colorsys(src)

View File

@ -65,21 +65,20 @@ class TestImage:
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None:
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="unrecognized image mode"):
Image.new(mode, (1, 1))
assert str(e.value) == "unrecognized image mode"
def test_exception_inheritance(self) -> None:
assert issubclass(UnidentifiedImageError, OSError)
def test_sanity(self) -> None:
im = Image.new("L", (100, 100))
assert repr(im)[:45] == "<PIL.Image.Image image mode=L size=100x100 at"
assert repr(im).startswith("<PIL.Image.Image image mode=L size=100x100 at")
assert im.mode == "L"
assert im.size == (100, 100)
im = Image.new("RGB", (100, 100))
assert repr(im)[:45] == "<PIL.Image.Image image mode=RGB size=100x100 "
assert repr(im).startswith("<PIL.Image.Image image mode=RGB size=100x100 ")
assert im.mode == "RGB"
assert im.size == (100, 100)
@ -658,6 +657,7 @@ class TestImage:
im.putpalette(list(range(256)) * 4, "RGBA")
im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped)
assert im.palette is not None
assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode

View File

@ -234,6 +234,7 @@ def test_gif_with_rgba_palette_to_p() -> None:
with Image.open("Tests/images/hopper.gif") as im:
im.info["transparency"] = 255
im.load()
assert im.palette is not None
assert im.palette.mode == "RGB"
im_p = im.convert("P")

View File

@ -47,6 +47,7 @@ class TestImageTransform:
transformed = im.transform(
im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0]
)
assert im.palette is not None
assert im.palette.palette == transformed.palette.palette
def test_extent(self) -> None:

View File

@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X
POINTS = (
((10, 10), (20, 40), (30, 30)),
[(10, 10), (20, 40), (30, 30)],
([10, 10], [20, 40], [30, 30]),
[[10, 10], [20, 40], [30, 30]],
(10, 10, 20, 40, 30, 30),
[10, 10, 20, 40, 30, 30],
)
@ -46,6 +48,8 @@ POINTS = (
KITE_POINTS = (
((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
[(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]),
[[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]],
)
@ -448,7 +452,6 @@ def test_shape1() -> None:
x3, y3 = 95, 5
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -470,7 +473,6 @@ def test_shape2() -> None:
x3, y3 = 5, 95
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -489,7 +491,6 @@ def test_transform() -> None:
draw = ImageDraw.Draw(im)
# Act
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.line(0, 0)
s.transform((0, 0, 0, 0, 0, 0))
@ -1047,8 +1048,8 @@ def create_base_image_draw(
background2: tuple[int, int, int] = GRAY,
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
img = Image.new(mode, size, background1)
for x in range(0, size[0]):
for y in range(0, size[1]):
for x in range(size[0]):
for y in range(size[1]):
if (x + y) % 2 == 0:
img.putpixel((x, y), background2)
return img, ImageDraw.Draw(img)
@ -1526,7 +1527,6 @@ def test_same_color_outline(bbox: Coords) -> None:
x2, y2 = 95, 50
x3, y3 = 95, 5
assert ImageDraw.Outline is not None
s = ImageDraw.Outline()
s.move(x0, y0)
s.curve(x1, y1, x2, y2, x3, y3)
@ -1630,7 +1630,7 @@ def test_compute_regular_polygon_vertices(
0,
ValueError,
"bounding_circle should contain 2D coordinates "
"and a radius (e.g. (x, y, r) or ((x, y), r) )",
r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)",
),
(
3,
@ -1644,7 +1644,7 @@ def test_compute_regular_polygon_vertices(
((50, 50, 50), 25),
0,
ValueError,
"bounding_circle centre should contain 2D coordinates (e.g. (x, y))",
r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)",
),
(
3,
@ -1669,9 +1669,8 @@ def test_compute_regular_polygon_vertices_input_error_handling(
expected_error: type[Exception],
error_message: str,
) -> None:
with pytest.raises(expected_error) as e:
with pytest.raises(expected_error, match=error_message):
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message
def test_continuous_horizontal_edges_polygon() -> None:

View File

@ -176,9 +176,8 @@ class TestImageFile:
b"0" * ImageFile.SAFEBLOCK
) # only SAFEBLOCK bytes, so that the header is truncated
)
with pytest.raises(OSError) as e:
with pytest.raises(OSError, match="Truncated File Read"):
BmpImagePlugin.BmpImageFile(b)
assert str(e.value) == "Truncated File Read"
@skip_unless_feature("zlib")
def test_truncated_with_errors(self) -> None:

View File

@ -80,15 +80,12 @@ def test_lut(op: str) -> None:
def test_no_operator_loaded() -> None:
im = Image.new("L", (1, 1))
mop = ImageMorph.MorphOp()
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.apply(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.match(im)
assert str(e.value) == "No operator loaded"
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="No operator loaded"):
mop.save_lut("")
assert str(e.value) == "No operator loaded"
# Test the named patterns
@ -238,15 +235,12 @@ def test_incorrect_mode() -> None:
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8")
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.apply(im)
assert str(e.value) == "Image mode must be L"
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.match(im)
assert str(e.value) == "Image mode must be L"
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="Image mode must be L"):
mop.get_on_pixels(im)
assert str(e.value) == "Image mode must be L"
def test_add_patterns() -> None:
@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None:
lb.add_patterns(new_patterns)
# Act / Assert
with pytest.raises(Exception) as e:
with pytest.raises(
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
):
lb.build_lut()
assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
def test_load_invalid_mrl() -> None:
@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None:
mop = ImageMorph.MorphOp()
# Act / Assert
with pytest.raises(Exception) as e:
with pytest.raises(Exception, match="Wrong size operator file!"):
mop.load_lut(invalid_mrl)
assert str(e.value) == "Wrong size operator file!"
def test_roundtrip_mrl(tmp_path: Path) -> None:

View File

@ -448,6 +448,15 @@ def test_exif_transpose() -> None:
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_with_xmp_tuple() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3
im.info["xmp"] = (b"test",)
transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif()
def test_exif_transpose_xml_without_xmp() -> None:
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
assert im.getexif()[0x0112] == 3

View File

@ -17,6 +17,7 @@ def test_sanity() -> None:
def test_reload() -> None:
with Image.open("Tests/images/hopper.gif") as im:
original = im.copy()
assert im.palette is not None
im.palette.dirty = 1
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
@ -111,7 +112,7 @@ def test_make_linear_lut() -> None:
assert isinstance(lut, list)
assert len(lut) == 256
# Check values
for i in range(0, len(lut)):
for i in range(len(lut)):
assert lut[i] == i

View File

@ -68,25 +68,10 @@ def test_path_constructors(
assert list(p) == [(0.0, 1.0)]
@pytest.mark.parametrize(
"coords",
(
("a", "b"),
([0, 1],),
[[0, 1]],
([0.0, 1.0],),
[[0.0, 1.0]],
),
)
def test_invalid_path_constructors(
coords: tuple[str, str] | Sequence[Sequence[int]],
) -> None:
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
# Assert
assert str(e.value) == "incorrect coordinate type"
def test_invalid_path_constructors() -> None:
# Arrange / Act
with pytest.raises(ValueError, match="incorrect coordinate type"):
ImagePath.Path(("a", "b"))
@pytest.mark.parametrize(
@ -99,13 +84,9 @@ def test_invalid_path_constructors(
),
)
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
# Act
with pytest.raises(ValueError) as e:
with pytest.raises(ValueError, match="wrong number of coordinates"):
ImagePath.Path(coords)
# Assert
assert str(e.value) == "wrong number of coordinates"
@pytest.mark.parametrize(
"coords, expected",

View File

@ -32,7 +32,7 @@ def test_sanity(tmp_path: Path) -> None:
def test_iterator() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
i = ImageSequence.Iterator(im)
for index in range(0, im.n_frames):
for index in range(im.n_frames):
assert i[index] == next(i)
with pytest.raises(IndexError):
i[index + 1]

View File

@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
("Tests/images/itxt_chunks.png", None),
],
)
@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_image(
tmp_path: Path, test_file: str, test_mode: str | None, protocol: int
) -> None:
@ -92,7 +92,7 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
im = im.convert("PA")
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
im._mode = "LA"
with open(filename, "wb") as f:
pickle.dump(im, f, protocol)
@ -133,7 +133,7 @@ def helper_assert_pickled_font_images(
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_string(protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
@ -147,7 +147,7 @@ def test_pickle_font_string(protocol: int) -> None:
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
# Arrange
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)

View File

@ -22,7 +22,7 @@ import PIL
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "8.1"
needs_sphinx = "8.2"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH').
nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
nitpick_ignore = [("py:class", "_CmsProfileCompatible")]
# -- Options for HTML output ----------------------------------------------

View File

@ -285,7 +285,7 @@ Image.register_decoder("DXT5", DXT5Decoder)
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS "
return prefix.startswith(b"DDS ")
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)

View File

@ -454,7 +454,8 @@ The :py:meth:`~PIL.Image.open` method may set the following
Raw EXIF data from the image.
**comment**
A comment about the image.
A comment about the image, from the COM marker. This is separate from the
UserComment tag that may be stored in the EXIF data.
.. versionadded:: 7.1.0
@ -1162,9 +1163,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
**append_images**
A list of images to append as additional frames. Each of the
images in the list can be single or multiframe images. Note however, that for
correct results, all the appended images should have the same
``encoderinfo`` and ``encoderconfig`` properties.
images in the list can be single or multiframe images.
.. versionadded:: 4.2.0

View File

@ -54,7 +54,7 @@ true color.
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM"
return prefix.startswith(b"SPAM")
class SpamImageFile(ImageFile.ImageFile):

View File

@ -44,14 +44,14 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0**
* **libfreetype** provides type related services
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
above uses liblcms2. Tested with **1.19** and **2.7-2.17**.
* **libwebp** provides the WebP format.

View File

@ -43,7 +43,7 @@ dynamic = [
optional-dependencies.docs = [
"furo",
"olefile",
"sphinx>=8.1",
"sphinx>=8.2",
"sphinx-copybutton",
"sphinx-inline-tabs",
"sphinxext-opengraph",
@ -104,7 +104,6 @@ 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"
@ -122,6 +121,7 @@ lint.select = [
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PT", # flake8-pytest-style
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa)
@ -134,6 +134,7 @@ lint.ignore = [
"E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ','
"PIE790", # flake8-pie: unnecessary-placeholder
"PT001", # pytest-fixture-incorrect-parentheses-style
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad

View File

@ -26,17 +26,6 @@ from typing import BinaryIO
from . import FontFile, Image
bdf_slant = {
"R": "Roman",
"I": "Italic",
"O": "Oblique",
"RI": "Reverse Italic",
"RO": "Reverse Oblique",
"OT": "Other",
}
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
def bdf_char(
f: BinaryIO,
@ -54,7 +43,7 @@ def bdf_char(
s = f.readline()
if not s:
return None
if s[:9] == b"STARTCHAR":
if s.startswith(b"STARTCHAR"):
break
id = s[9:].strip().decode("ascii")
@ -62,7 +51,7 @@ def bdf_char(
props = {}
while True:
s = f.readline()
if not s or s[:6] == b"BITMAP":
if not s or s.startswith(b"BITMAP"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
@ -71,7 +60,7 @@ def bdf_char(
bitmap = bytearray()
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
if not s or s.startswith(b"ENDCHAR"):
break
bitmap += s[:-1]
@ -107,7 +96,7 @@ class BdfFontFile(FontFile.FontFile):
super().__init__()
s = fp.readline()
if s[:13] != b"STARTFONT 2.1":
if not s.startswith(b"STARTFONT 2.1"):
msg = "not a valid BDF file"
raise SyntaxError(msg)
@ -116,7 +105,7 @@ class BdfFontFile(FontFile.FontFile):
while True:
s = fp.readline()
if not s or s[:13] == b"ENDPROPERTIES":
if not s or s.startswith(b"ENDPROPERTIES"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")

View File

@ -246,7 +246,7 @@ class BLPFormatError(NotImplementedError):
def _accept(prefix: bytes) -> bool:
return prefix[:4] in (b"BLP1", b"BLP2")
return prefix.startswith((b"BLP1", b"BLP2"))
class BlpImageFile(ImageFile.ImageFile):
@ -291,7 +291,7 @@ class BlpImageFile(ImageFile.ImageFile):
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
class _BLPBaseDecoder(ImageFile.PyDecoder):
class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:

View File

@ -50,7 +50,7 @@ BIT2MODE = {
def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM"
return prefix.startswith(b"BM")
def _dib_accept(prefix: bytes) -> bool:

View File

@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
return prefix.startswith((b"BUFR", b"ZCZC"))
class BufrStubImageFile(ImageFile.StubImageFile):

View File

@ -26,7 +26,7 @@ from ._binary import i32le as i32
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\0\0\2\0"
return prefix.startswith(b"\0\0\2\0")
##

View File

@ -564,7 +564,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS "
return prefix.startswith(b"DDS ")
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)

View File

@ -170,7 +170,9 @@ def Ghostscript(
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
return prefix.startswith(b"%!PS") or (
len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5
)
##
@ -295,7 +297,7 @@ class EpsImageFile(ImageFile.ImageFile):
m = field.match(s)
if m:
k = m.group(1)
if k[:8] == "PS-Adobe":
if k.startswith("PS-Adobe"):
self.info["PS-Adobe"] = k[9:]
else:
self.info[k] = ""

View File

@ -17,7 +17,7 @@ from . import Image, ImageFile
def _accept(prefix: bytes) -> bool:
return prefix[:6] == b"SIMPLE"
return prefix.startswith(b"SIMPLE")
class FitsImageFile(ImageFile.ImageFile):

View File

@ -42,7 +42,7 @@ MODES = {
def _accept(prefix: bytes) -> bool:
return prefix[:8] == olefile.MAGIC
return prefix.startswith(olefile.MAGIC)
##

View File

@ -79,8 +79,6 @@ class FtexImageFile(ImageFile.ImageFile):
self._size = struct.unpack("<2i", self.fp.read(8))
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
self._mode = "RGB"
# Only support single-format files.
# I don't know of any multi-format file.
assert format_count == 1
@ -95,6 +93,7 @@ class FtexImageFile(ImageFile.ImageFile):
self._mode = "RGBA"
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
elif format == Format.UNCOMPRESSED:
self._mode = "RGB"
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")]
else:
msg = f"Invalid texture compression format: {repr(format)}"
@ -108,7 +107,7 @@ class FtexImageFile(ImageFile.ImageFile):
def _accept(prefix: bytes) -> bool:
return prefix[:4] == MAGIC
return prefix.startswith(MAGIC)
Image.register_open(FtexImageFile.format, FtexImageFile, _accept)

View File

@ -56,7 +56,7 @@ class GdImageFile(ImageFile.ImageFile):
msg = "Not a valid GD 2.x .gd file"
raise SyntaxError(msg)
self._mode = "L" # FIXME: "P"
self._mode = "P"
self._size = i16(s, 2), i16(s, 4)
true_color = s[6]
@ -68,14 +68,14 @@ class GdImageFile(ImageFile.ImageFile):
self.info["transparency"] = tindex
self.palette = ImagePalette.raw(
"XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4]
"RGBX", s[7 + true_color_offset + 6 : 7 + true_color_offset + 6 + 256 * 4]
)
self.tile = [
ImageFile._Tile(
"raw",
(0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4,
7 + true_color_offset + 6 + 256 * 4,
"L",
)
]

View File

@ -67,7 +67,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
def _accept(prefix: bytes) -> bool:
return prefix[:6] in [b"GIF87a", b"GIF89a"]
return prefix.startswith((b"GIF87a", b"GIF89a"))
##
@ -257,7 +257,7 @@ class GifImageFile(ImageFile.ImageFile):
# application extension
#
info["extension"] = block, self.fp.tell()
if block[:11] == b"NETSCAPE2.0":
if block.startswith(b"NETSCAPE2.0"):
block = self.data()
if block and len(block) >= 3 and block[0] == 1:
self.info["loop"] = i16(block, 1)
@ -689,16 +689,21 @@ def _write_multiple_frames(
im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
continue
if im_frames[-1].encoderinfo.get("disposal") == 2:
if background_im is None:
color = im.encoderinfo.get(
"transparency", im.info.get("transparency", (0, 0, 0))
)
background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background)
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]
# To appear correctly in viewers using a convention,
# only consider transparency, and not background color
color = im.encoderinfo.get(
"transparency", im.info.get("transparency")
)
if color is not None:
if background_im is None:
background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background)
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]
else:
bbox = (0, 0) + im_frame.size
elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo:
assert im_frame.palette is not None
@ -764,7 +769,8 @@ def _write_multiple_frames(
if not palette:
frame_data.encoderinfo["include_color_table"] = True
im_frame = im_frame.crop(frame_data.bbox)
if frame_data.bbox != (0, 0) + im_frame.size:
im_frame = im_frame.crop(frame_data.bbox)
offset = frame_data.bbox[:2]
_write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
return True

View File

@ -116,7 +116,7 @@ class GimpGradientFile(GradientFile):
"""File handler for GIMP's gradient format."""
def __init__(self, fp: IO[bytes]) -> None:
if fp.readline()[:13] != b"GIMP Gradient":
if not fp.readline().startswith(b"GIMP Gradient"):
msg = "not a GIMP gradient file"
raise SyntaxError(msg)

View File

@ -29,7 +29,7 @@ class GimpPaletteFile:
def __init__(self, fp: IO[bytes]) -> None:
palette = [o8(i) * 3 for i in range(256)]
if fp.readline()[:12] != b"GIMP Palette":
if not fp.readline().startswith(b"GIMP Palette"):
msg = "not a GIMP palette file"
raise SyntaxError(msg)

View File

@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"GRIB" and prefix[7] == 1
return prefix.startswith(b"GRIB") and prefix[7] == 1
class GribStubImageFile(ImageFile.StubImageFile):

View File

@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
def _accept(prefix: bytes) -> bool:
return prefix[:8] == b"\x89HDF\r\n\x1a\n"
return prefix.startswith(b"\x89HDF\r\n\x1a\n")
class HDF5StubImageFile(ImageFile.StubImageFile):

View File

@ -117,14 +117,13 @@ def read_png_or_jpeg2000(
sig = fobj.read(12)
im: Image.Image
if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"):
fobj.seek(start)
im = PngImagePlugin.PngImageFile(fobj)
Image._decompression_bomb_check(im.size)
return {"RGBA": im}
elif (
sig[:4] == b"\xff\x4f\xff\x51"
or sig[:4] == b"\x0d\x0a\x87\x0a"
sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a"))
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
):
if not enable_jpeg2k:
@ -387,7 +386,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
def _accept(prefix: bytes) -> bool:
return prefix[:4] == MAGIC
return prefix.startswith(MAGIC)
Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)

View File

@ -118,7 +118,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
def _accept(prefix: bytes) -> bool:
return prefix[:4] == _MAGIC
return prefix.startswith(_MAGIC)
class IconHeader(NamedTuple):

View File

@ -155,9 +155,9 @@ class ImImageFile(ImageFile.ImageFile):
msg = "not an IM file"
raise SyntaxError(msg)
if s[-2:] == b"\r\n":
if s.endswith(b"\r\n"):
s = s[:-2]
elif s[-1:] == b"\n":
elif s.endswith(b"\n"):
s = s[:-1]
try:
@ -209,7 +209,7 @@ class ImImageFile(ImageFile.ImageFile):
self._mode = self.info[MODE]
# Skip forward to start of image data
while s and s[:1] != b"\x1a":
while s and not s.startswith(b"\x1a"):
s = self.fp.read(1)
if not s:
msg = "File truncated"
@ -247,7 +247,7 @@ class ImImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack
if self.rawmode[:2] == "F;":
if self.rawmode.startswith("F;"):
# ifunc95 formats
try:
# use bit decoder (if necessary)

View File

@ -1001,7 +1001,7 @@ class Image:
elif len(mode) == 3:
transparency = tuple(
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
for i in range(0, len(transparency))
for i in range(len(transparency))
)
new_im.info["transparency"] = transparency
return new_im
@ -2475,7 +2475,21 @@ class Image:
format to use is determined from the filename extension.
If a file object was used instead of a filename, this
parameter should always be used.
:param params: Extra parameters to the image writer.
:param params: Extra parameters to the image writer. These can also be
set on the image itself through ``encoderinfo``. This is useful when
saving multiple images::
# Saving XMP data to a single image
from PIL import Image
red = Image.new("RGB", (1, 1), "#f00")
red.save("out.mpo", xmp=b"test")
# Saving XMP data to the second frame of an image
from PIL import Image
black = Image.new("RGB", (1, 1))
red = Image.new("RGB", (1, 1), "#f00")
red.encoderinfo = {"xmp": b"test"}
black.save("out.mpo", save_all=True, append_images=[red])
:returns: None
:exception ValueError: If the output format could not be determined
from the file name. Use the format option to solve this.
@ -2966,7 +2980,7 @@ class Image:
# Abstract handlers.
class ImagePointHandler:
class ImagePointHandler(abc.ABC):
"""
Used as a mixin by point transforms
(for use with :py:meth:`~PIL.Image.Image.point`)
@ -2977,7 +2991,7 @@ class ImagePointHandler:
pass
class ImageTransformHandler:
class ImageTransformHandler(abc.ABC):
"""
Used as a mixin by geometry transforms
(for use with :py:meth:`~PIL.Image.Image.transform`)
@ -2996,15 +3010,6 @@ class ImageTransformHandler:
# --------------------------------------------------------------------
# Factories
#
# Debugging
def _wedge() -> Image:
"""Create grayscale wedge (for debugging only)"""
return Image()._new(core.wedge("L"))
def _check_size(size: Any) -> None:
"""
@ -4007,12 +4012,12 @@ class Exif(_ExifBase):
if tag == ExifTags.IFD.MakerNote:
from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data[:8] == b"FUJIFILM":
if tag_data.startswith(b"FUJIFILM"):
ifd_offset = i32le(tag_data, 8)
ifd_data = tag_data[ifd_offset:]
makernote = {}
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]):
for i in range(struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
)
@ -4047,7 +4052,7 @@ class Exif(_ExifBase):
self._ifds[tag] = dict(self._fixup_dict(makernote))
elif self.get(0x010F) == "Nintendo":
makernote = {}
for i in range(0, struct.unpack(">H", tag_data[:2])[0]):
for i in range(struct.unpack(">H", tag_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
)

View File

@ -42,11 +42,7 @@ from ._deprecate import deprecate
from ._typing import Coords
# experimental access to the outline API
Outline: Callable[[], Image.core._Outline] | None
try:
Outline = Image.core.outline
except AttributeError:
Outline = None
Outline: Callable[[], Image.core._Outline] = Image.core.outline
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
@ -1208,7 +1204,7 @@ def _compute_regular_polygon_vertices(
degrees = 360 / n_sides
# Start with the bottom left polygon vertex
current_angle = (270 - 0.5 * degrees) + rotation
for _ in range(0, n_sides):
for _ in range(n_sides):
angles.append(current_angle)
current_angle += degrees
if current_angle > 360:
@ -1231,4 +1227,4 @@ def _color_diff(
first = color1 if isinstance(color1, tuple) else (color1,)
second = color2 if isinstance(color2, tuple) else (color2,)
return sum(abs(first[i] - second[i]) for i in range(0, len(second)))
return sum(abs(first[i] - second[i]) for i in range(len(second)))

View File

@ -438,7 +438,7 @@ class ImageFile(Image.Image):
return self.tell() != frame
class StubHandler:
class StubHandler(abc.ABC):
def open(self, im: StubImageFile) -> None:
pass
@ -447,7 +447,7 @@ class StubHandler:
pass
class StubImageFile(ImageFile):
class StubImageFile(ImageFile, metaclass=abc.ABCMeta):
"""
Base class for stub image loaders.
@ -455,9 +455,9 @@ class StubImageFile(ImageFile):
certain format, but relies on external code to load the file.
"""
@abc.abstractmethod
def _open(self) -> None:
msg = "StubImageFile subclass must implement _open"
raise NotImplementedError(msg)
pass
def load(self) -> Image.core.PixelAccess | None:
loader = self._load()
@ -471,10 +471,10 @@ class StubImageFile(ImageFile):
self.__dict__ = image.__dict__
return image.load()
@abc.abstractmethod
def _load(self) -> StubHandler | None:
"""(Hook) Find actual image loader."""
msg = "StubImageFile subclass must implement _load"
raise NotImplementedError(msg)
pass
class Parser:

View File

@ -27,7 +27,7 @@ if TYPE_CHECKING:
from ._typing import NumpyArray
class Filter:
class Filter(abc.ABC):
@abc.abstractmethod
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
pass

View File

@ -213,14 +213,14 @@ def colorize(
blue = []
# Create the low-end values
for i in range(0, blackpoint):
for i in range(blackpoint):
red.append(rgb_black[0])
green.append(rgb_black[1])
blue.append(rgb_black[2])
# Create the mapping (2-color)
if rgb_mid is None:
range_map = range(0, whitepoint - blackpoint)
range_map = range(whitepoint - blackpoint)
for i in range_map:
red.append(
@ -235,8 +235,8 @@ def colorize(
# Create the mapping (3-color)
else:
range_map1 = range(0, midpoint - blackpoint)
range_map2 = range(0, whitepoint - midpoint)
range_map1 = range(midpoint - blackpoint)
range_map2 = range(whitepoint - midpoint)
for i in range_map1:
red.append(
@ -256,7 +256,7 @@ def colorize(
blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
# Create the high-end values
for i in range(0, 256 - whitepoint):
for i in range(256 - whitepoint):
red.append(rgb_white[0])
green.append(rgb_white[1])
blue.append(rgb_white[2])
@ -729,11 +729,15 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
r"<tiff:Orientation>([0-9])</tiff:Orientation>",
):
value = exif_image.info[key]
exif_image.info[key] = (
re.sub(pattern, "", value)
if isinstance(value, str)
else re.sub(pattern.encode(), b"", value)
)
if isinstance(value, str):
value = re.sub(pattern, "", value)
elif isinstance(value, tuple):
value = tuple(
re.sub(pattern.encode(), b"", v) for v in value
)
else:
value = re.sub(pattern.encode(), b"", value)
exif_image.info[key] = value
if not in_place:
return transposed_image
elif not in_place:

View File

@ -192,7 +192,7 @@ if sys.platform == "darwin":
register(MacViewer)
class UnixViewer(Viewer):
class UnixViewer(abc.ABC, Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}

View File

@ -28,7 +28,7 @@ from __future__ import annotations
import tkinter
from io import BytesIO
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any
from . import Image, ImageFile
@ -263,28 +263,3 @@ def getimage(photo: PhotoImage) -> Image.Image:
_pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())
return im
def _show(image: Image.Image, title: str | None) -> None:
"""Helper for the Image.show method."""
class UI(tkinter.Label):
def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None:
self.image: BitmapImage | PhotoImage
if im.mode == "1":
self.image = BitmapImage(im, foreground="white", master=master)
else:
self.image = PhotoImage(im, master=master)
if TYPE_CHECKING:
image = cast(tkinter._Image, self.image)
else:
image = self.image
super().__init__(master, image=image, bg="black", bd=0)
if not getattr(tkinter, "_default_root"):
msg = "tkinter not initialized"
raise OSError(msg)
top = tkinter.Toplevel()
if title:
top.title(title)
UI(top, image).pack()

View File

@ -352,9 +352,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
def _accept(prefix: bytes) -> bool:
return (
prefix[:4] == b"\xff\x4f\xff\x51"
or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
return prefix.startswith(
(b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a")
)

View File

@ -77,7 +77,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
self.app[app] = s # compatibility
self.applist.append((app, s))
if marker == 0xFFE0 and s[:4] == b"JFIF":
if marker == 0xFFE0 and s.startswith(b"JFIF"):
# extract JFIF information
self.info["jfif"] = version = i16(s, 5) # version
self.info["jfif_version"] = divmod(version, 256)
@ -95,19 +95,19 @@ def APP(self: JpegImageFile, marker: int) -> None:
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":
elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"):
# extract EXIF information
if "exif" in self.info:
self.info["exif"] += s[6:]
else:
self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE1 and s[:29] == b"http://ns.adobe.com/xap/1.0/\x00":
elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"):
self.info["xmp"] = s.split(b"\x00", 1)[1]
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
elif marker == 0xFFE2 and s.startswith(b"FPXR\0"):
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
elif marker == 0xFFE2 and s[:12] == b"ICC_PROFILE\0":
elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"):
# Since an ICC profile can be larger than the maximum size of
# a JPEG marker (64K), we need provisions to split it into
# multiple markers. The format defined by the ICC specifies
@ -120,7 +120,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
# reassemble the profile, rather than assuming that the APP2
# markers appear in the correct sequence.
self.icclist.append(s)
elif marker == 0xFFED and s[:14] == b"Photoshop 3.0\x00":
elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"):
# parse the image resource block
offset = 14
photoshop = self.info.setdefault("photoshop", {})
@ -153,7 +153,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
except struct.error:
break # insufficient data
elif marker == 0xFFEE and s[:5] == b"Adobe":
elif marker == 0xFFEE and s.startswith(b"Adobe"):
self.info["adobe"] = i16(s, 5)
# extract Adobe custom properties
try:
@ -162,7 +162,7 @@ def APP(self: JpegImageFile, marker: int) -> None:
pass
else:
self.info["adobe_transform"] = adobe_transform
elif marker == 0xFFE2 and s[:4] == b"MPF\0":
elif marker == 0xFFE2 and s.startswith(b"MPF\0"):
# extract MPO information
self.info["mp"] = s[4:]
# offset is current location minus buffer size
@ -325,7 +325,7 @@ MARKER = {
def _accept(prefix: bytes) -> bool:
# Magic number was taken from https://en.wikipedia.org/wiki/JPEG
return prefix[:3] == b"\xff\xd8\xff"
return prefix.startswith(b"\xff\xd8\xff")
##
@ -547,7 +547,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
return None
file_contents = io.BytesIO(data)
head = file_contents.read(8)
endianness = ">" if head[:4] == b"\x4d\x4d\x00\x2a" else "<"
endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<"
# process dictionary
from . import TiffImagePlugin
@ -569,7 +569,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
mpentries = []
try:
rawmpentries = mp[0xB002]
for entrynum in range(0, quant):
for entrynum in range(quant):
unpackedentry = struct.unpack_from(
f"{endianness}LLLHH", rawmpentries, entrynum * 16
)

View File

@ -23,7 +23,7 @@ from . import Image, ImageFile
def _accept(prefix: bytes) -> bool:
return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04"
return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04")
##

View File

@ -26,7 +26,7 @@ from . import Image, TiffImagePlugin
def _accept(prefix: bytes) -> bool:
return prefix[:8] == olefile.MAGIC
return prefix.startswith(olefile.MAGIC)
##
@ -54,7 +54,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.images = [
path
for path in self.ole.listdir()
if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image"
if path[1:] and path[0].endswith(".ACI") and path[1] == "Image"
]
# if we didn't find any images, this is probably not
@ -73,12 +73,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
try:
filename = self.images[frame]
except IndexError as e:
msg = "no such frame"
raise EOFError(msg) from e
filename = self.images[frame]
self.fp = self.ole.openstream(filename)
TiffImagePlugin.TiffImageFile._open(self)

View File

@ -54,7 +54,7 @@ class BitStream:
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\x00\x00\x01\xb3"
return prefix.startswith(b"\x00\x00\x01\xb3")
##

View File

@ -37,7 +37,7 @@ from ._binary import o16le as o16
def _accept(prefix: bytes) -> bool:
return prefix[:4] in [b"DanM", b"LinS"]
return prefix.startswith((b"DanM", b"LinS"))
##
@ -69,7 +69,7 @@ class MspImageFile(ImageFile.ImageFile):
self._mode = "1"
self._size = i16(s, 4), i16(s, 6)
if s[:4] == b"DanM":
if s.startswith(b"DanM"):
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")]
else:
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)]

View File

@ -32,7 +32,7 @@ class PaletteFile:
if not s:
break
if s[:1] == b"#":
if s.startswith(b"#"):
continue
if len(s) > 100:
msg = "bad palette file"

View File

@ -34,7 +34,7 @@ class PcdImageFile(ImageFile.ImageFile):
self.fp.seek(2048)
s = self.fp.read(2048)
if s[:4] != b"PCD_":
if not s.startswith(b"PCD_"):
msg = "not a PCD file"
raise SyntaxError(msg)

View File

@ -28,7 +28,7 @@ from ._binary import i16le as i16
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\200\350\000\000"
return prefix.startswith(b"\200\350\000\000")
##

View File

@ -740,7 +740,7 @@ class PngStream(ChunkStream):
def _accept(prefix: bytes) -> bool:
return prefix[:8] == _MAGIC
return prefix.startswith(_MAGIC)
##

View File

@ -47,7 +47,7 @@ MODES = {
def _accept(prefix: bytes) -> bool:
return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"
return prefix.startswith(b"P") and prefix[1] in b"0123456fy"
##

View File

@ -47,7 +47,7 @@ MODES = {
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"8BPS"
return prefix.startswith(b"8BPS")
##
@ -169,15 +169,11 @@ class PsdImageFile(ImageFile.ImageFile):
return
# seek to given layer (1..max)
try:
_, mode, _, tile = self.layers[layer - 1]
self._mode = mode
self.tile = tile
self.frame = layer
self.fp = self._fp
except IndexError as e:
msg = "no such layer"
raise EOFError(msg) from e
_, mode, _, tile = self.layers[layer - 1]
self._mode = mode
self.tile = tile
self.frame = layer
self.fp = self._fp
def tell(self) -> int:
# return layer number (0=image, 1..max=layers)

View File

@ -14,7 +14,7 @@ from ._binary import i32be as i32
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"qoif"
return prefix.startswith(b"qoif")
class QoiImageFile(ImageFile.ImageFile):

View File

@ -288,7 +288,7 @@ if not getattr(Image.core, "libtiff_support_custom_tags", True):
def _accept(prefix: bytes) -> bool:
return prefix[:4] in PREFIXES
return prefix.startswith(tuple(PREFIXES))
def _limit_rational(
@ -404,7 +404,7 @@ class IFDRational(Rational):
def __repr__(self) -> str:
return str(float(self._val))
def __hash__(self) -> int:
def __hash__(self) -> int: # type: ignore[override]
return self._val.__hash__()
def __eq__(self, other: object) -> bool:
@ -1280,7 +1280,7 @@ class TiffImageFile(ImageFile.ImageFile):
blocks = {}
val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val:
while val[:4] == b"8BIM":
while val.startswith(b"8BIM"):
id = i16(val[4:6])
n = math.ceil((val[6] + 1) / 2) * 2
size = i32(val[6 + n : 10 + n])
@ -1584,7 +1584,7 @@ class TiffImageFile(ImageFile.ImageFile):
# byte order.
elif rawmode == "I;16":
rawmode = "I;16N"
elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
elif rawmode.endswith((";16B", ";16L")):
rawmode = rawmode[:-1] + "N"
# Offset in the tile tuple is 0, we go from 0,0 to
@ -2295,9 +2295,7 @@ class AppendingTiffWriter(io.BytesIO):
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy()
encoderconfig = im.encoderconfig
append_images = list(encoderinfo.get("append_images", []))
append_images = list(im.encoderinfo.get("append_images", []))
if not hasattr(im, "n_frames") and not append_images:
return _save(im, fp, filename)
@ -2305,12 +2303,11 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try:
with AppendingTiffWriter(fp) as tf:
for ims in [im] + append_images:
ims.encoderinfo = encoderinfo
ims.encoderconfig = encoderconfig
if not hasattr(ims, "n_frames"):
nfr = 1
else:
nfr = ims.n_frames
if not hasattr(ims, "encoderinfo"):
ims.encoderinfo = {}
if not hasattr(ims, "encoderconfig"):
ims.encoderconfig = ()
nfr = getattr(ims, "n_frames", 1)
for idx in range(nfr):
ims.seek(idx)

View File

@ -21,7 +21,7 @@ _VP8_MODES_BY_IDENTIFIER = {
def _accept(prefix: bytes) -> bool | str:
is_riff_file_format = prefix[:4] == b"RIFF"
is_riff_file_format = prefix.startswith(b"RIFF")
is_webp_file = prefix[8:12] == b"WEBP"
is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER
@ -46,8 +46,7 @@ class WebPImageFile(ImageFile.ImageFile):
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
# Get info from decoder
width, height, loop_count, bgcolor, frame_count, mode = self._decoder.get_info()
self._size = width, height
self._size, loop_count, bgcolor, frame_count, mode = self._decoder.get_info()
self.info["loop"] = loop_count
bg_a, bg_r, bg_g, bg_b = (
(bgcolor >> 24) & 0xFF,

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