Merge branch 'main' into mode_enums

This commit is contained in:
Andrew Murray 2025-02-20 08:16:17 +11:00 committed by GitHub
commit cad6b3ddac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
127 changed files with 1537 additions and 1429 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

View File

@ -10,15 +10,11 @@ brew install \
ghostscript \
jpeg-turbo \
libimagequant \
libraqm \
libtiff \
little-cms2 \
openjpeg \
webp
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
brew install libraqm
fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage

View File

@ -35,10 +35,6 @@ 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,
@ -56,9 +52,13 @@ 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

View File

@ -60,7 +60,6 @@ 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:

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.1.0
LIBPNG_VERSION=1.6.45
HARFBUZZ_VERSION=10.2.0
LIBPNG_VERSION=1.6.46
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
XZ_VERSION=5.6.4
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
ZLIB_NG_VERSION=2.2.3
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

@ -1,17 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6
rev: v0.9.4
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.10.0
rev: 25.1.0
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.8.0
rev: 1.8.2
hooks:
- id: bandit
args: [--severity-level=high]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.6
rev: v19.1.7
hooks:
- id: clang-format
types: [c]
@ -50,14 +50,14 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.30.0
rev: 0.31.1
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.0.0
rev: v1.3.0
hooks:
- id: zizmor
@ -78,7 +78,7 @@ repos:
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.4.1
rev: 1.5.0
hooks:
- id: tox-ini-fmt

View File

@ -3,26 +3,25 @@ from __future__ import annotations
import zlib
from io import BytesIO
import pytest
from PIL import Image, ImageFile, PngImagePlugin
TEST_FILE = "Tests/images/png_decompression_dos.png"
def test_ignore_dos_text() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
try:
im = Image.open(TEST_FILE)
with Image.open(TEST_FILE) as im:
im.load()
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert isinstance(im, PngImagePlugin.PngImageFile)
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
assert isinstance(im, PngImagePlugin.PngImageFile)
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
for s in im.info.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
for s in im.info.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_text() -> None:

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -19,7 +19,7 @@ except ImportError:
class TestColorLut3DCoreAPI:
def generate_identity_table(
self, channels: int, size: int | tuple[int, int, int]
) -> tuple[int, int, int, int, list[float]]:
) -> tuple[int, tuple[int, int, int], list[float]]:
if isinstance(size, tuple):
size_1d, size_2d, size_3d = size
else:
@ -39,9 +39,7 @@ class TestColorLut3DCoreAPI:
]
return (
channels,
size_1d,
size_2d,
size_3d,
(size_1d, size_2d, size_3d),
[item for sublist in table for item in sublist],
)
@ -89,21 +87,21 @@ class TestColorLut3DCoreAPI:
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 7
)
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 9
)
with pytest.raises(TypeError):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, "0"] * 8
)
with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16)
@pytest.mark.parametrize(
"lut_mode, table_channels, table_size",
@ -264,7 +262,7 @@ class TestColorLut3DCoreAPI:
assert_image_equal(
Image.merge('RGB', im.split()[::-1]),
im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2, [
3, (2, 2, 2), [
0, 0, 0, 0, 0, 1,
0, 1, 0, 0, 1, 1,
@ -286,7 +284,7 @@ class TestColorLut3DCoreAPI:
# fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2,
3, (2, 2, 2),
[
-1, -1, -1, 2, -1, -1,
-1, 2, -1, 2, 2, -1,
@ -307,7 +305,7 @@ class TestColorLut3DCoreAPI:
# fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2,
3, (2, 2, 2),
[
-3, -3, -3, 5, -3, -3,
-3, 5, -3, 5, 5, -3,

View File

@ -12,19 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb:
def teardown_method(self) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self) -> None:
# Implicit assert: no warning.
# A warning would cause a failure.
with Image.open(TEST_FILE):
pass
def test_no_warning_no_limit(self) -> None:
def test_no_warning_no_limit(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
# Turn limit off
Image.MAX_IMAGE_PIXELS = None
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
assert Image.MAX_IMAGE_PIXELS is None
# Act / Assert
@ -33,18 +30,18 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE):
pass
def test_warning(self) -> None:
def test_warning(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Set limit to trigger warning on the test file
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 128 * 128 - 1)
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
with pytest.warns(Image.DecompressionBombWarning):
with Image.open(TEST_FILE):
pass
def test_exception(self) -> None:
def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 64 * 128 - 1)
assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1
with pytest.raises(Image.DecompressionBombError):
@ -66,9 +63,9 @@ class TestDecompressionBomb:
with pytest.raises(Image.DecompressionBombError):
im.seek(1)
def test_exception_gif_zero_width(self) -> None:
def test_exception_gif_zero_width(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Set limit to trigger exception on the test file
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 4 * 64 * 128)
assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
with pytest.raises(Image.DecompressionBombError):

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

@ -35,32 +35,29 @@ def test_sanity() -> None:
assert im.is_animated
def test_prefix_chunk() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with Image.open(animated_test_file_with_prefix_chunk) as im:
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
assert im.info["duration"] == 171
assert im.is_animated
def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(animated_test_file_with_prefix_chunk) as im:
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
assert im.info["duration"] == 171
assert im.is_animated
palette = im.getpalette()
assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0]
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
palette = im.getpalette()
assert palette[3:6] == [255, 255, 255]
assert palette[381:384] == [204, 204, 12]
assert palette[765:] == [252, 0, 0]
@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:

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

@ -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:
@ -86,12 +83,12 @@ def test_invalid_file() -> None:
def test_l_mode_transparency() -> None:
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L"
assert im.load()[0, 0] == 128
assert im.getpixel((0, 0)) == 128
assert im.info["transparency"] == 255
im.seek(1)
assert im.mode == "L"
assert im.load()[0, 0] == 128
assert im.getpixel((0, 0)) == 128
def test_l_mode_after_rgb() -> None:
@ -109,7 +106,7 @@ def test_palette_not_needed_for_second_frame() -> None:
assert_image_similar(im, hopper("L").convert("RGB"), 8)
def test_strategy() -> None:
def test_strategy(monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")
@ -119,35 +116,36 @@ def test_strategy() -> None:
im.seek(1)
expected_different = im.convert("RGB")
try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_rgb_always)
monkeypatch.setattr(
GifImagePlugin, "LOADING_STRATEGY", GifImagePlugin.LoadingStrategy.RGB_ALWAYS
)
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_rgb_always)
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)
GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
)
# Stay in P mode with only a global palette
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "P"
monkeypatch.setattr(
GifImagePlugin,
"LOADING_STRATEGY",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
)
# Stay in P mode with only a global palette
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_different)
im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_different)
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "P"
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "RGB"
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
im.seek(1)
assert im.mode == "RGB"
def test_optimize() -> None:
@ -309,8 +307,9 @@ 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").load()[0, 0]
original_color = im.convert("RGB").getpixel((0, 0))
im.seek(1)
assert im.mode == mode
@ -318,10 +317,10 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None:
im = im.convert("RGB")
# Check a color only from the old palette
assert im.load()[0, 0] == original_color
assert im.getpixel((0, 0)) == original_color
# Check a color from the new palette
assert im.load()[24, 24] not in first_frame_colors
assert im.getpixel((24, 24)) not in first_frame_colors
def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
@ -487,8 +486,7 @@ def test_eoferror() -> None:
def test_first_frame_transparency() -> None:
with Image.open("Tests/images/first_frame_transparency.gif") as im:
px = im.load()
assert px[0, 0] == im.info["transparency"]
assert im.getpixel((0, 0)) == im.info["transparency"]
def test_dispose_none() -> None:
@ -528,6 +526,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
@ -555,17 +554,15 @@ def test_dispose_background_transparency() -> None:
def test_transparent_dispose(
loading_strategy: GifImagePlugin.LoadingStrategy,
expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],
monkeypatch: pytest.MonkeyPatch,
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
def test_dispose_previous() -> None:
@ -764,6 +761,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:
@ -1313,6 +1325,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)
@ -1347,32 +1360,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:
@ -1398,24 +1409,23 @@ def test_lzw_bits() -> None:
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
test_file: str,
loading_strategy: GifImagePlugin.LoadingStrategy,
monkeypatch: pytest.MonkeyPatch,
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
im.seek(1)
assert im.size == (150, 150)
im.seek(1)
assert im.size == (150, 150)
im.load()
assert im.im.size == (150, 150)
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
im.load()
assert im.im.size == (150, 150)
def test_missing_background() -> 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:
@ -243,26 +245,23 @@ def test_draw_reloaded(tmp_path: Path) -> None:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_truncated_mask() -> None:
def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
data = data[:-3]
try:
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "1"
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "1"
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
data = output.getvalue()[:-1]
data = output.getvalue()[:-1]
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"

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

@ -530,12 +530,13 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_truncated_jpeg_should_read_all_the_data(self) -> None:
def test_truncated_jpeg_should_read_all_the_data(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
filename = "Tests/images/truncated_jpeg.jpg"
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(filename) as im:
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
assert im.getbbox() is not None
def test_truncated_jpeg_throws_oserror(self) -> None:
@ -933,7 +934,7 @@ class TestFileJpeg:
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer = BytesIO(b"\xff" * size) # Many xff bytes
max_pos = 0
orig_read = buffer.read
@ -1024,7 +1025,7 @@ class TestFileJpeg:
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1)
def test_eof(self) -> None:
def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Even though this decoder never says that it is finished
# the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
@ -1039,9 +1040,8 @@ class TestFileJpeg:
im.tile = [
ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
]
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_separate_tables(self) -> None:
im = hopper()

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)
@ -181,14 +182,11 @@ def test_load_dpi() -> None:
assert "dpi" not in im.info
def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
# JPEG2000 image with a restricted ICC profile and a known colorspace
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_restricted_icc_profile(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
# JPEG2000 image with a restricted ICC profile and a known colorspace
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
assert im.mode == "RGB"
@pytest.mark.skipif(
@ -424,6 +422,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
@ -431,6 +430,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

@ -309,7 +309,7 @@ class TestFileLibTiff(LibTiffTestCase):
}
def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str],
) -> None:
im = hopper()
@ -1103,13 +1103,15 @@ class TestFileLibTiff(LibTiffTestCase):
)
def test_buffering(self, test_file: str) -> None:
# load exif first
with Image.open(open(test_file, "rb", buffering=1048576)) as im:
exif = dict(im.getexif())
with open(test_file, "rb", buffering=1048576) as f:
with Image.open(f) as im:
exif = dict(im.getexif())
# load image before exif
with Image.open(open(test_file, "rb", buffering=1048576)) as im2:
im2.load()
exif_after_load = dict(im2.getexif())
with open(test_file, "rb", buffering=1048576) as f:
with Image.open(f) as im2:
im2.load()
exif_after_load = dict(im2.getexif())
assert exif == exif_after_load
@ -1156,23 +1158,22 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(im.tag_v2[STRIPOFFSETS]) > 1
@pytest.mark.parametrize("argument", (True, False))
def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None:
def test_save_single_strip(
self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif")
if not argument:
TiffImagePlugin.STRIP_SIZE = 2**18
try:
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, "TIFF", **arguments)
monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18)
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[STRIPOFFSETS]) == 1
finally:
TiffImagePlugin.STRIP_SIZE = 65536
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[STRIPOFFSETS]) == 1
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
def test_save_zero(self, compression: str | None, 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

@ -264,7 +264,7 @@ def test_pdf_append(tmp_path: Path) -> None:
# append some info
pdf.info.Title = "abc"
pdf.info.Author = "def"
pdf.info.Subject = "ghi\uABCD"
pdf.info.Subject = "ghi\uabcd"
pdf.info.Keywords = "qw)e\\r(ty"
pdf.info.Creator = "hopper()"
pdf.start_writing()
@ -292,7 +292,7 @@ def test_pdf_append(tmp_path: Path) -> None:
assert pdf.info.Title == "abc"
assert pdf.info.Producer == "PdfParser"
assert pdf.info.Keywords == "qw)e\\r(ty"
assert pdf.info.Subject == "ghi\uABCD"
assert pdf.info.Subject == "ghi\uabcd"
assert b"CreationDate" in pdf.info
assert b"ModDate" in pdf.info
check_pdf_pages_consistency(pdf)

View File

@ -363,7 +363,7 @@ class TestFilePng:
with pytest.raises((OSError, SyntaxError)):
im.verify()
def test_verify_ignores_crc_error(self) -> None:
def test_verify_ignores_crc_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
# check ignores crc errors in ancillary chunks
chunk_data = chunk(b"tEXt", b"spam")
@ -373,24 +373,20 @@ class TestFilePng:
with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(BytesIO(image_data))
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
im = load(image_data)
assert im is not None
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im = load(image_data)
assert im is not None
def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None:
def test_verify_not_ignores_crc_error_in_required_chunk(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# check does not ignore crc errors in required chunks
image_data = MAGIC + IHDR[:-1] + b"q" + TAIL
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(BytesIO(image_data))
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with pytest.raises(SyntaxError):
PngImagePlugin.PngImageFile(BytesIO(image_data))
def test_roundtrip_dpi(self) -> None:
# Check dpi roundtripping
@ -600,7 +596,7 @@ class TestFilePng:
(b"prIV", b"VALUE3", True),
]
def test_textual_chunks_after_idat(self) -> None:
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.png") as im:
assert "comment" in im.text
for k, v in {
@ -614,18 +610,17 @@ class TestFilePng:
with pytest.raises(OSError):
assert isinstance(im.text, dict)
# Raises an EOFError in load_end
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
# Raises a UnicodeDecodeError in load_end
with Image.open("Tests/images/truncated_image.png") as im:
# The file is truncated
with pytest.raises(OSError):
im.text
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
assert isinstance(im.text, dict)
ImageFile.LOAD_TRUNCATED_IMAGES = False
# Raises an EOFError in load_end
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
def test_unknown_compression_method(self) -> None:
with pytest.raises(SyntaxError, match="Unknown compression method"):
@ -651,15 +646,16 @@ class TestFilePng:
@pytest.mark.parametrize(
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
)
def test_truncated_chunks(self, cid: bytes) -> None:
def test_truncated_chunks(
self, cid: bytes, monkeypatch: pytest.MonkeyPatch
) -> None:
fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png:
with pytest.raises(ValueError):
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.parametrize("save_all", (True, False))
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
@ -789,17 +785,14 @@ class TestFilePng:
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
def test_truncated_end_chunk(self) -> None:
def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
with pytest.raises(OSError):
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png")
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@ -808,11 +801,11 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
mem_limit = 2 * 1024 # max increase in K
iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs
def test_leak_load(self) -> None:
def test_leak_load(self, monkeypatch: pytest.MonkeyPatch) -> None:
with open("Tests/images/hopper.png", "rb") as f:
DATA = BytesIO(f.read(16 * 1024))
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
with Image.open(DATA) as im:
im.load()
@ -820,7 +813,4 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
with Image.open(DATA) as im:
im.load()
try:
self._test_leak(core)
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
self._test_leak(core)

View File

@ -49,7 +49,7 @@ def test_sanity() -> None:
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
# P6 with maxval < 255
(
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0a\x0f\x10\x11",
"RGB",
(
(0, 15, 30),
@ -60,7 +60,7 @@ def test_sanity() -> None:
# P6 with maxval > 255
(
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xff\xff",
"RGB",
(
(0, 1, 2),
@ -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

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():
@ -746,7 +746,7 @@ class TestFileTiff:
assert reread.n_frames == 3
def test_fixoffsets(self) -> None:
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
b.seek(0)
a.fixOffsets(1, isShort=True)
@ -759,14 +759,14 @@ class TestFileTiff:
with pytest.raises(RuntimeError):
a.fixOffsets(1)
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**16
b.seek(0)
a.fixOffsets(1, isShort=True)
b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
b = BytesIO(b"II\x2b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**32
@ -777,18 +777,20 @@ class TestFileTiff:
a.fixOffsets(1, isLong=True)
def test_appending_tiff_writer_writelong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.seek(-4, os.SEEK_CUR)
a.writeLong(2**32 - 1)
assert b.getvalue() == data + b"\xff\xff\xff\xff"
assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff"
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.seek(-2, os.SEEK_CUR)
a.rewriteLastShortToLong(2**32 - 1)
assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff"
assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff"
def test_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set.
@ -939,11 +941,10 @@ class TestFileTiff:
@pytest.mark.timeout(6)
@pytest.mark.filterwarnings("ignore:Truncated File Read")
def test_timeout(self) -> None:
def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/timeout-6646305047838720") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.parametrize(
"test_file",

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

@ -28,9 +28,9 @@ except ImportError:
class TestUnsupportedWebp:
def test_unsupported(self) -> None:
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
if HAVE_WEBP:
WebPImagePlugin.SUPPORTED = False
monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False)
file_path = "Tests/images/hopper.webp"
with pytest.warns(UserWarning):
@ -38,9 +38,6 @@ class TestUnsupportedWebp:
with Image.open(file_path):
pass
if HAVE_WEBP:
WebPImagePlugin.SUPPORTED = True
@skip_unless_feature("webp")
class TestFileWebp:

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:
@ -71,7 +73,7 @@ def test_load_float_dpi() -> None:
with open("Tests/images/drawing.emf", "rb") as fp:
data = fp.read()
b = BytesIO(data[:8] + b"\x06\xFA" + data[10:])
b = BytesIO(data[:8] + b"\x06\xfa" + data[10:])
with Image.open(b) as im:
assert im.info["dpi"][0] == 2540

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

@ -74,12 +74,12 @@ class TestImage:
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)
@ -578,9 +578,7 @@ class TestImage:
def test_one_item_tuple(self) -> None:
for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,))
px = im.load()
assert px is not None
assert px[0, 0] == 5
assert im.getpixel((0, 0)) == 5
def test_linear_gradient_wrong_mode(self) -> None:
# Arrange
@ -660,6 +658,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

@ -222,9 +222,7 @@ def test_l_macro_rounding(convert_mode: str) -> None:
im.palette.getcolor((0, 1, 2))
converted_im = im.convert(convert_mode)
px = converted_im.load()
assert px is not None
converted_color = px[0, 0]
converted_color = converted_im.getpixel((0, 0))
if convert_mode == "LA":
assert isinstance(converted_color, tuple)
converted_color = converted_color[0]
@ -236,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

@ -148,10 +148,8 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color)
converted = im.quantize(method=method)
converted_px = converted.load()
assert converted_px is not None
assert converted.palette is not None
assert converted_px[0, 0] == converted.palette.colors[color]
assert converted.getpixel((0, 0)) == converted.palette.colors[color]
def test_small_palette() -> None:

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

@ -812,7 +812,7 @@ def test_rounded_rectangle(
tuple[int, int, int, int]
| tuple[list[int]]
| tuple[tuple[int, int], tuple[int, int]]
)
),
) -> None:
# Arrange
im = Image.new("RGB", (200, 200))
@ -1396,6 +1396,28 @@ def test_stroke_descender() -> None:
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
@skip_unless_feature("freetype2")
def test_stroke_inside_gap() -> None:
# Arrange
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
draw.text((12, 12), "i", "#f00", font, stroke_width=20)
# Assert
for y in range(im.height):
glyph = ""
for x in range(im.width):
if im.getpixel((x, y)) == (0, 0, 0):
if glyph == "started":
glyph = "ended"
else:
assert glyph != "ended", "Gap inside stroked glyph"
glyph = "started"
@skip_unless_feature("freetype2")
def test_split_word() -> None:
# Arrange

View File

@ -191,13 +191,10 @@ class TestImageFile:
im.load()
@skip_unless_feature("zlib")
def test_truncated_without_errors(self) -> None:
def test_truncated_without_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/truncated_image.png") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
im.load()
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
@skip_unless_feature("zlib")
def test_broken_datastream_with_errors(self) -> None:
@ -206,13 +203,12 @@ class TestImageFile:
im.load()
@skip_unless_feature("zlib")
def test_broken_datastream_without_errors(self) -> None:
def test_broken_datastream_without_errors(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
with Image.open("Tests/images/broken_data_stream.png") as im:
ImageFile.LOAD_TRUNCATED_IMAGES = True
try:
im.load()
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
im.load()
class MockPyDecoder(ImageFile.PyDecoder):

View File

@ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
@pytest.mark.parametrize(
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
"align, ext",
(("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")),
)
def test_render_multiline_text_align(
font: ImageFont.FreeTypeFont, align: str, ext: str
@ -461,6 +462,20 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
assert mask.size == (108, 13)
def test_stroke_mask() -> None:
# Arrange
text = "i"
# Act
font = ImageFont.truetype(FONT_PATH, 128)
mask = font.getmask(text, stroke_width=2)
# Assert
assert mask.getpixel((34, 5)) == 255
assert mask.getpixel((38, 5)) == 0
assert mask.getpixel((42, 5)) == 255
def test_load_when_image_not_found() -> None:
with tempfile.NamedTemporaryFile(delete=False) as tmp:
pass
@ -543,7 +558,7 @@ def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
# issue #3777
text = "A\u278A\U0001F12B"
text = "A\u278a\U0001f12b"
target = "Tests/images/unicode_extended.png"
ttf = ImageFont.truetype(
@ -1012,7 +1027,7 @@ def test_sbix(layout_engine: ImageFont.Layout) -> None:
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)
d.text((50, 50), "\uE901", font=font, embedded_color=True)
d.text((50, 50), "\ue901", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
except OSError as e: # pragma: no cover
@ -1029,7 +1044,7 @@ def test_sbix_mask(layout_engine: ImageFont.Layout) -> None:
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)
d.text((50, 50), "\uE901", (100, 0, 0), font=font)
d.text((50, 50), "\ue901", (100, 0, 0), font=font)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
except OSError as e: # pragma: no cover

View File

@ -229,7 +229,7 @@ def test_getlength(
@pytest.mark.parametrize("direction", ("ltr", "ttb"))
@pytest.mark.parametrize(
"text",
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
("i" + ("\u030c" * 15) + "i", "i" + "\u032c" * 15 + "i", "\u035cii", "i\u0305i"),
ids=("caron-above", "caron-below", "double-breve", "overline"),
)
def test_getlength_combine(mode: str, direction: str, text: str) -> None:
@ -272,27 +272,27 @@ def test_anchor_ttb(anchor: str) -> None:
combine_tests = (
# extends above (e.g. issue #4553)
("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08),
("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08),
("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08),
("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08),
("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3),
("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3),
("caron", "a\u030c\u030c\u030c\u030c\u030cb", None, None, 0.08),
("caron_la", "a\u030c\u030c\u030c\u030c\u030cb", "la", None, 0.08),
("caron_lt", "a\u030c\u030c\u030c\u030c\u030cb", "lt", None, 0.08),
("caron_ls", "a\u030c\u030c\u030c\u030c\u030cb", "ls", None, 0.08),
("caron_ttb", "ca" + ("\u030c" * 15) + "b", None, "ttb", 0.3),
("caron_ttb_lt", "ca" + ("\u030c" * 15) + "b", "lt", "ttb", 0.3),
# extends below
("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02),
("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02),
("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02),
("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02),
("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03),
("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03),
("caron_below", "a\u032c\u032c\u032c\u032c\u032cb", None, None, 0.02),
("caron_below_ld", "a\u032c\u032c\u032c\u032c\u032cb", "ld", None, 0.02),
("caron_below_lb", "a\u032c\u032c\u032c\u032c\u032cb", "lb", None, 0.02),
("caron_below_ls", "a\u032c\u032c\u032c\u032c\u032cb", "ls", None, 0.02),
("caron_below_ttb", "a" + ("\u032c" * 15) + "b", None, "ttb", 0.03),
("caron_below_ttb_lb", "a" + ("\u032c" * 15) + "b", "lb", "ttb", 0.03),
# extends to the right (e.g. issue #3745)
("double_breve_below", "a\u035Ci", None, None, 0.02),
("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02),
("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02),
("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02),
("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02),
("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02),
("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02),
("double_breve_below", "a\u035ci", None, None, 0.02),
("double_breve_below_ma", "a\u035ci", "ma", None, 0.02),
("double_breve_below_ra", "a\u035ci", "ra", None, 0.02),
("double_breve_below_ttb", "a\u035cb", None, "ttb", 0.02),
("double_breve_below_ttb_rt", "a\u035cb", "rt", "ttb", 0.02),
("double_breve_below_ttb_mt", "a\u035cb", "mt", "ttb", 0.02),
("double_breve_below_ttb_st", "a\u035cb", "st", "ttb", 0.02),
# extends to the left (fail=0.064)
("overline", "i\u0305", None, None, 0.02),
("overline_la", "i\u0305", "la", None, 0.02),
@ -346,7 +346,7 @@ def test_combine_multiline(anchor: str, align: str) -> None:
path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word
text = "i\u0305\u035c\ntext" # i with overline and double breve, and a word
im = Image.new("RGB", (400, 400), "white")
d = ImageDraw.Draw(im)

View File

@ -165,14 +165,10 @@ def test_pad() -> None:
def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
px = new_im.load()
assert px is not None
assert px[2, 0] == 1
assert new_im.getpixel((2, 0)) == 1
new_im = ImageOps.pad(im, (1, 4))
px = new_im.load()
assert px is not None
assert px[0, 2] == 1
assert new_im.getpixel((0, 2)) == 1
@pytest.mark.parametrize("mode", ("P", "PA"))

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"))
@ -189,7 +190,7 @@ def test_2bit_palette(tmp_path: Path) -> None:
rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2
img = Image.frombytes("P", (6, 1), rgb)
img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB
img.putpalette(b"\xff\x00\x00\x00\xff\x00\x00\x00\xff") # RGB
img.save(outfile, format="PNG")
assert_image_equal_tofile(img, outfile)

View File

@ -79,7 +79,7 @@ def test_path_constructors(
),
)
def test_invalid_path_constructors(
coords: tuple[str, str] | Sequence[Sequence[int]]
coords: tuple[str, str] | Sequence[Sequence[int]],
) -> None:
# Act
with pytest.raises(ValueError) as e:

View File

@ -7,36 +7,30 @@ import pytest
from PIL import Image
def test_overflow() -> None:
def test_overflow(monkeypatch: pytest.MonkeyPatch) -> None:
# There is the potential to overflow comparisons in map.c
# if there are > SIZE_MAX bytes in the image or if
# the file encodes an offset that makes
# (offset + size(bytes)) > SIZE_MAX
# Note that this image triggers the decompression bomb warning:
max_pixels = Image.MAX_IMAGE_PIXELS
Image.MAX_IMAGE_PIXELS = None
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
# This image hits the offset test.
with Image.open("Tests/images/l2rgb_read.bmp") as im:
with pytest.raises((ValueError, MemoryError, OSError)):
im.load()
Image.MAX_IMAGE_PIXELS = max_pixels
def test_tobytes() -> None:
def test_tobytes(monkeypatch: pytest.MonkeyPatch) -> None:
# Note that this image triggers the decompression bomb warning:
max_pixels = Image.MAX_IMAGE_PIXELS
Image.MAX_IMAGE_PIXELS = None
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
# Previously raised an access violation on Windows
with Image.open("Tests/images/l2rgb_read.bmp") as im:
with pytest.raises((ValueError, MemoryError, OSError)):
im.tobytes()
Image.MAX_IMAGE_PIXELS = max_pixels
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_ysize() -> None:

View File

@ -141,9 +141,7 @@ def test_save_tiff_uint16() -> None:
a.shape = TEST_IMAGE_SIZE
img = Image.fromarray(a)
img_px = img.load()
assert img_px is not None
assert img_px[0, 0] == pixel_value
assert img.getpixel((0, 0)) == pixel_value
@pytest.mark.parametrize(

View File

@ -20,10 +20,10 @@ from PIL.PdfParser import (
def test_text_encode_decode() -> None:
assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c"
assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc"
assert encode_text("abc") == b"\xfe\xff\x00a\x00b\x00c"
assert decode_text(b"\xfe\xff\x00a\x00b\x00c") == "abc"
assert decode_text(b"abc") == "abc"
assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD"
assert decode_text(b"\x1b a \x1c") == "\u02d9 a \u02dd"
def test_indirect_refs() -> None:
@ -45,8 +45,8 @@ def test_parsing() -> None:
assert PdfParser.get_value(b"false%", 0) == (False, 5)
assert PdfParser.get_value(b"null<", 0) == (None, 4)
assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15)
assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1F\xA3", 8)
assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17)
assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1f\xa3", 8)
assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1f\xa0", 17)
assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5)
assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13)
assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14)
@ -56,9 +56,9 @@ def test_parsing() -> None:
assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12)
assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12)
assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7)
assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6)
assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5)
assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6)
assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2b", 6)
assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2b", 5)
assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2ba", 6)
assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7)
assert PdfParser.get_value(b" 123 (", 0) == (123, 4)
assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0
@ -118,7 +118,7 @@ def test_pdf_repr() -> None:
assert pdf_repr(None) == b"null"
assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)"
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
assert pdf_repr(PdfBinary(b"\x90\x1f\xa0")) == b"<901FA0>"
def test_duplicate_xref_entry() -> None:

View File

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.3
archive_version=4.3.4
archive=$archive_name-$archive_version

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

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

@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3.3**
* Pillow has been tested with libimagequant **2.6-4.3.4**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.

View File

@ -387,8 +387,11 @@ Methods
the number of pixels between lines.
:param align: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.
@ -455,8 +458,11 @@ Methods
of Pillow, but implemented only in version 8.0.0.
:param spacing: The number of pixels between lines.
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.
@ -599,8 +605,11 @@ Methods
the number of pixels between lines.
:param align: If the text is passed on to
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.
@ -650,8 +659,11 @@ Methods
vertical text. See :ref:`text-anchors` for details.
This parameter is ignored for non-TrueType fonts.
:param spacing: The number of pixels between lines.
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
Use the ``anchor`` parameter to specify the alignment to ``xy``.
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
the relative alignment of lines. Use the ``anchor`` parameter to
specify the alignment to ``xy``.
.. versionadded:: 11.2.0 ``"justify"``
:param direction: Direction of the text. It can be ``"rtl"`` (right to
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
Requires libraqm.

View File

@ -44,6 +44,18 @@ TODO
API Additions
=============
``"justify"`` multiline text alignment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be
aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
from PIL import Image, ImageDraw
im = Image.new("RGB", (50, 25))
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")
Check for MozJPEG
^^^^^^^^^^^^^^^^^

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"

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):

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

@ -10,6 +10,7 @@
#
from __future__ import annotations
import os
from typing import IO
from . import Image, ImageFile
@ -32,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):
@ -40,13 +41,11 @@ class BufrStubImageFile(ImageFile.StubImageFile):
format_description = "BUFR"
def _open(self) -> None:
offset = self.fp.tell()
if not _accept(self.fp.read(4)):
msg = "Not a BUFR file"
raise SyntaxError(msg)
self.fp.seek(offset)
self.fp.seek(-4, os.SEEK_CUR)
# make something up
self._mode = "F"

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

@ -108,7 +108,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

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

@ -10,6 +10,7 @@
#
from __future__ import annotations
import os
from typing import IO
from . import Image, ImageFile
@ -32,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):
@ -40,13 +41,11 @@ class GribStubImageFile(ImageFile.StubImageFile):
format_description = "GRIB"
def _open(self) -> None:
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
msg = "Not a GRIB file"
raise SyntaxError(msg)
self.fp.seek(offset)
self.fp.seek(-8, os.SEEK_CUR)
# make something up
self._mode = "F"

View File

@ -10,6 +10,7 @@
#
from __future__ import annotations
import os
from typing import IO
from . import Image, ImageFile
@ -32,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):
@ -40,13 +41,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
format_description = "HDF5"
def _open(self) -> None:
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
msg = "Not an HDF file"
raise SyntaxError(msg)
self.fp.seek(offset)
self.fp.seek(-8, os.SEEK_CUR)
# make something up
self._mode = "F"

View File

@ -117,14 +117,14 @@ 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")
or sig.startswith(b"\x0d\x0a\x87\x0a")
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
):
if not enable_jpeg2k:
@ -387,7 +387,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

@ -145,7 +145,7 @@ class ImImageFile(ImageFile.ImageFile):
if s == b"\r":
continue
if not s or s == b"\0" or s == b"\x1A":
if not s or s == b"\0" or s == b"\x1a":
break
# FIXME: this may read whole file if not a text 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

@ -514,7 +514,7 @@ class ImagePointTransform:
def _getscaleoffset(
expr: Callable[[ImagePointTransform], ImagePointTransform | float]
expr: Callable[[ImagePointTransform], ImagePointTransform | float],
) -> tuple[float, float]:
a = expr(ImagePointTransform(1, 0))
return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)
@ -2996,15 +2996,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:
"""
@ -3884,7 +3875,7 @@ class Exif(_ExifBase):
return self._fixup_dict(dict(info))
def _get_head(self) -> bytes:
version = b"\x2B" if self.bigtiff else b"\x2A"
version = b"\x2b" if self.bigtiff else b"\x2a"
if self.endian == "<":
head = b"II" + version + b"\x00" + o32le(8)
else:
@ -4007,7 +3998,7 @@ 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:]

View File

@ -557,21 +557,6 @@ class ImageDraw:
return split_character in text
def _multiline_split(self, text: AnyStr) -> list[AnyStr]:
return text.split("\n" if isinstance(text, str) else b"\n")
def _multiline_spacing(
self,
font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
spacing: float,
stroke_width: float,
) -> float:
return (
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
+ stroke_width
+ spacing
)
def text(
self,
xy: tuple[float, float],
@ -643,6 +628,7 @@ class ImageDraw:
features=features,
language=language,
stroke_width=stroke_width,
stroke_filled=True,
anchor=anchor,
ink=ink,
start=start,
@ -692,11 +678,125 @@ class ImageDraw:
draw_text(stroke_ink, stroke_width)
# Draw normal text
draw_text(ink, 0)
if ink != stroke_ink:
draw_text(ink)
else:
# Only draw normal text
draw_text(ink)
def _prepare_multiline_text(
self,
xy: tuple[float, float],
text: AnyStr,
font: (
ImageFont.ImageFont
| ImageFont.FreeTypeFont
| ImageFont.TransposedFont
| None
),
anchor: str | None,
spacing: float,
align: str,
direction: str | None,
features: list[str] | None,
language: str | None,
stroke_width: float,
embedded_color: bool,
font_size: float | None,
) -> tuple[
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont,
str,
list[tuple[tuple[float, float], AnyStr]],
]:
if direction == "ttb":
msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg)
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
msg = "anchor must be a 2 character string"
raise ValueError(msg)
elif anchor[1] in "tb":
msg = "anchor not supported for multiline text"
raise ValueError(msg)
if font is None:
font = self._getfont(font_size)
widths = []
max_width: float = 0
lines = text.split("\n" if isinstance(text, str) else b"\n")
line_spacing = (
self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3]
+ stroke_width
+ spacing
)
for line in lines:
line_width = self.textlength(
line,
font,
direction=direction,
features=features,
language=language,
embedded_color=embedded_color,
)
widths.append(line_width)
max_width = max(max_width, line_width)
top = xy[1]
if anchor[1] == "m":
top -= (len(lines) - 1) * line_spacing / 2.0
elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing
parts = []
for idx, line in enumerate(lines):
left = xy[0]
width_difference = max_width - widths[idx]
# first align left by anchor
if anchor[0] == "m":
left -= width_difference / 2.0
elif anchor[0] == "r":
left -= width_difference
# then align by align parameter
if align in ("left", "justify"):
pass
elif align == "center":
left += width_difference / 2.0
elif align == "right":
left += width_difference
else:
msg = 'align must be "left", "center", "right" or "justify"'
raise ValueError(msg)
if align == "justify" and width_difference != 0:
words = line.split(" " if isinstance(text, str) else b" ")
word_widths = [
self.textlength(
word,
font,
direction=direction,
features=features,
language=language,
embedded_color=embedded_color,
)
for word in words
]
width_difference = max_width - sum(word_widths)
for i, word in enumerate(words):
parts.append(((left, top), word))
left += word_widths[i] + width_difference / (len(words) - 1)
else:
parts.append(((left, top), line))
top += line_spacing
return font, anchor, parts
def multiline_text(
self,
xy: tuple[float, float],
@ -720,62 +820,24 @@ class ImageDraw:
*,
font_size: float | None = None,
) -> None:
if direction == "ttb":
msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg)
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
msg = "anchor must be a 2 character string"
raise ValueError(msg)
elif anchor[1] in "tb":
msg = "anchor not supported for multiline text"
raise ValueError(msg)
if font is None:
font = self._getfont(font_size)
widths = []
max_width: float = 0
lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:
line_width = self.textlength(
line, font, direction=direction, features=features, language=language
)
widths.append(line_width)
max_width = max(max_width, line_width)
top = xy[1]
if anchor[1] == "m":
top -= (len(lines) - 1) * line_spacing / 2.0
elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing
for idx, line in enumerate(lines):
left = xy[0]
width_difference = max_width - widths[idx]
# first align left by anchor
if anchor[0] == "m":
left -= width_difference / 2.0
elif anchor[0] == "r":
left -= width_difference
# then align by align parameter
if align == "left":
pass
elif align == "center":
left += width_difference / 2.0
elif align == "right":
left += width_difference
else:
msg = 'align must be "left", "center" or "right"'
raise ValueError(msg)
font, anchor, lines = self._prepare_multiline_text(
xy,
text,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
embedded_color,
font_size,
)
for xy, line in lines:
self.text(
(left, top),
xy,
line,
fill,
font,
@ -787,7 +849,6 @@ class ImageDraw:
stroke_fill=stroke_fill,
embedded_color=embedded_color,
)
top += line_spacing
def textlength(
self,
@ -889,69 +950,26 @@ class ImageDraw:
*,
font_size: float | None = None,
) -> tuple[float, float, float, float]:
if direction == "ttb":
msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg)
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
msg = "anchor must be a 2 character string"
raise ValueError(msg)
elif anchor[1] in "tb":
msg = "anchor not supported for multiline text"
raise ValueError(msg)
if font is None:
font = self._getfont(font_size)
widths = []
max_width: float = 0
lines = self._multiline_split(text)
line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:
line_width = self.textlength(
line,
font,
direction=direction,
features=features,
language=language,
embedded_color=embedded_color,
)
widths.append(line_width)
max_width = max(max_width, line_width)
top = xy[1]
if anchor[1] == "m":
top -= (len(lines) - 1) * line_spacing / 2.0
elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing
font, anchor, lines = self._prepare_multiline_text(
xy,
text,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
embedded_color,
font_size,
)
bbox: tuple[float, float, float, float] | None = None
for idx, line in enumerate(lines):
left = xy[0]
width_difference = max_width - widths[idx]
# first align left by anchor
if anchor[0] == "m":
left -= width_difference / 2.0
elif anchor[0] == "r":
left -= width_difference
# then align by align parameter
if align == "left":
pass
elif align == "center":
left += width_difference / 2.0
elif align == "right":
left += width_difference
else:
msg = 'align must be "left", "center" or "right"'
raise ValueError(msg)
for xy, line in lines:
bbox_line = self.textbbox(
(left, top),
xy,
line,
font,
anchor,
@ -971,8 +989,6 @@ class ImageDraw:
max(bbox[3], bbox_line[3]),
)
top += line_spacing
if bbox is None:
return xy[0], xy[1], xy[0], xy[1]
return bbox

View File

@ -598,8 +598,6 @@ class Color3DLUT(MultibandFilter):
self.mode or image.mode,
Image.Resampling.BILINEAR,
self.channels,
self.size[0],
self.size[1],
self.size[2],
self.size,
self.table,
)

View File

@ -644,10 +644,10 @@ class FreeTypeFont:
features,
language,
stroke_width,
kwargs.get("stroke_filled", False),
anchor,
ink,
start[0],
start[1],
start,
)
def font_variant(

View File

@ -48,9 +48,9 @@ class AffineTransform(Transform):
Define an affine image transform.
This function takes a 6-tuple (a, b, c, d, e, f) which contain the first
two rows from an affine transform matrix. For each pixel (x, y) in the
output image, the new value is taken from a position (a x + b y + c,
d x + e y + f) in the input image, rounded to nearest pixel.
two rows from the inverse of an affine transform matrix. For each pixel
(x, y) in the output image, the new value is taken from a position (a x +
b y + c, d x + e y + f) in the input image, rounded to nearest pixel.
This function can be used to scale, translate, rotate, and shear the
original image.
@ -58,7 +58,7 @@ class AffineTransform(Transform):
See :py:meth:`.Image.transform`
:param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
from an affine transform matrix.
from the inverse of an affine transform matrix.
"""
method = Image.Transform.AFFINE

View File

@ -55,7 +55,7 @@ class ImtImageFile(ImageFile.ImageFile):
if not s:
break
if s == b"\x0C":
if s == b"\x0c":
# image data begins
self.tile = [
ImageFile._Tile(

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")
##
@ -342,7 +342,7 @@ class JpegImageFile(ImageFile.ImageFile):
if not _accept(s):
msg = "not a JPEG file"
raise SyntaxError(msg)
s = b"\xFF"
s = b"\xff"
# Create attributes
self.bits = self.layers = 0
@ -417,7 +417,7 @@ class JpegImageFile(ImageFile.ImageFile):
# Premature EOF.
# Pretend file is finished adding EOI marker
self._ended = True
return b"\xFF\xD9"
return b"\xff\xd9"
return s
@ -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
@ -712,7 +712,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
def validate_qtables(
qtables: (
str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None
)
),
) -> list[list[int]] | None:
if qtables is None:
return qtables
@ -769,7 +769,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
msg = "XMP data is too long"
raise ValueError(msg)
size = o16(2 + overhead_len + len(xmp))
extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
icc_profile = info.get("icc_profile")
if icc_profile:
@ -783,7 +783,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
for marker in markers:
size = o16(2 + overhead_len + len(marker))
extra += (
b"\xFF\xE2"
b"\xff\xe2"
+ size
+ b"ICC_PROFILE\0"
+ o8(i)
@ -816,8 +816,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
optimize,
info.get("keep_rgb", False),
info.get("streamtype", 0),
dpi[0],
dpi[1],
dpi,
subsampling,
info.get("restart_marker_blocks", 0),
info.get("restart_marker_rows", 0),

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

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

@ -51,7 +51,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if not offsets:
# APP2 marker
im_frame.encoderinfo["extra"] = (
b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82
b"\xff\xe2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82
)
exif = im_frame.encoderinfo.get("exif")
if isinstance(exif, Image.Exif):
@ -84,7 +84,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
ifd[0xB002] = mpentries
fp.seek(mpf_offset)
fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8))
fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8))
fp.seek(0, os.SEEK_END)

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

@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ o16(dpi[0])
+ o16(dpi[1])
+ b"\0" * 24
+ b"\xFF" * 24
+ b"\xff" * 24
+ b"\0"
+ o8(planes)
+ o16(stride)

View File

@ -19,14 +19,14 @@ def encode_text(s: str) -> bytes:
PDFDocEncoding = {
0x16: "\u0017",
0x18: "\u02D8",
0x19: "\u02C7",
0x1A: "\u02C6",
0x1B: "\u02D9",
0x1C: "\u02DD",
0x1D: "\u02DB",
0x1E: "\u02DA",
0x1F: "\u02DC",
0x18: "\u02d8",
0x19: "\u02c7",
0x1A: "\u02c6",
0x1B: "\u02d9",
0x1C: "\u02dd",
0x1D: "\u02db",
0x1E: "\u02da",
0x1F: "\u02dc",
0x80: "\u2022",
0x81: "\u2020",
0x82: "\u2021",
@ -36,29 +36,29 @@ PDFDocEncoding = {
0x86: "\u0192",
0x87: "\u2044",
0x88: "\u2039",
0x89: "\u203A",
0x89: "\u203a",
0x8A: "\u2212",
0x8B: "\u2030",
0x8C: "\u201E",
0x8D: "\u201C",
0x8E: "\u201D",
0x8C: "\u201e",
0x8D: "\u201c",
0x8E: "\u201d",
0x8F: "\u2018",
0x90: "\u2019",
0x91: "\u201A",
0x91: "\u201a",
0x92: "\u2122",
0x93: "\uFB01",
0x94: "\uFB02",
0x93: "\ufb01",
0x94: "\ufb02",
0x95: "\u0141",
0x96: "\u0152",
0x97: "\u0160",
0x98: "\u0178",
0x99: "\u017D",
0x99: "\u017d",
0x9A: "\u0131",
0x9B: "\u0142",
0x9C: "\u0153",
0x9D: "\u0161",
0x9E: "\u017E",
0xA0: "\u20AC",
0x9E: "\u017e",
0xA0: "\u20ac",
}

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)
##
@ -1382,7 +1382,7 @@ def _save(
b"\0", # 12: interlace flag
)
chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
if icc:
@ -1433,7 +1433,7 @@ def _save(
chunk(fp, b"tRNS", transparency[:alpha_bytes])
else:
transparency = max(0, min(255, transparency))
alpha = b"\xFF" * transparency + b"\0"
alpha = b"\xff" * transparency + b"\0"
chunk(fp, b"tRNS", alpha[:alpha_bytes])
elif im.mode in ("1", "L", "I", "I;16"):
transparency = max(0, min(65535, transparency))

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"
##
@ -230,7 +230,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
msg = b"Invalid token for this mode: %s" % bytes([token])
raise ValueError(msg)
data = (data + tokens)[:total_bytes]
invert = bytes.maketrans(b"01", b"\xFF\x00")
invert = bytes.maketrans(b"01", b"\xff\x00")
return data.translate(invert)
def _decode_blocks(self, maxval: int) -> bytearray:

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