mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-04-07 02:44:14 +03:00
Merge branch 'main' into reduce-contention
This commit is contained in:
commit
f91fcf34e1
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.22.0
|
||||
cibuildwheel==2.23.1
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mypy==1.14.1
|
||||
mypy==1.15.0
|
||||
IceSpringPySideStubs-PyQt6
|
||||
IceSpringPySideStubs-PySide6
|
||||
ipython
|
||||
|
|
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
|
@ -16,6 +16,6 @@
|
|||
}
|
||||
],
|
||||
"schedule": [
|
||||
"on the 3rd day of the month"
|
||||
"* * 3 * *"
|
||||
]
|
||||
}
|
||||
|
|
13
.github/workflows/test-docker.yml
vendored
13
.github/workflows/test-docker.yml
vendored
|
@ -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: |
|
||||
|
|
1
.github/workflows/test-mingw.yml
vendored
1
.github/workflows/test-mingw.yml
vendored
|
@ -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 \
|
||||
|
|
6
.github/workflows/test-windows.yml
vendored
6
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||
|
|
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
@ -41,6 +41,7 @@ jobs:
|
|||
"ubuntu-latest",
|
||||
]
|
||||
python-version: [
|
||||
"pypy3.11",
|
||||
"pypy3.10",
|
||||
"3.14",
|
||||
"3.13t",
|
||||
|
|
31
.github/workflows/wheels-dependencies.sh
vendored
31
.github/workflows/wheels-dependencies.sh
vendored
|
@ -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
|
||||
|
||||
|
|
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ----------------------------------------------
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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] = ""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -42,7 +42,7 @@ MODES = {
|
|||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:8] == olefile.MAGIC
|
||||
return prefix.startswith(olefile.MAGIC)
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -740,7 +740,7 @@ class PngStream(ChunkStream):
|
|||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:8] == _MAGIC
|
||||
return prefix.startswith(_MAGIC)
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user