Merge branch 'main' into bugreport

This commit is contained in:
Andrew Murray 2024-03-02 17:04:41 +11:00 committed by GitHub
commit 01fdf2ff51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 239 additions and 127 deletions

View File

@ -6,6 +6,7 @@ init:
# Uncomment previous line to get RDP access during the build.
environment:
COVERAGE_CORE: sysmon
EXECUTABLE: python.exe
TEST_OPTIONS:
DEPLOY: YES

View File

@ -7,10 +7,12 @@ on:
paths:
- ".github/workflows/docs.yml"
- "docs/**"
- "src/PIL/**"
pull_request:
paths:
- ".github/workflows/docs.yml"
- "docs/**"
- "src/PIL/**"
workflow_dispatch:
permissions:

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest
@ -64,10 +67,10 @@ jobs:
mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python3-setuptools \
mingw-w64-x86_64-python-pyqt6
python3 -m ensurepip
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
jobs:
build:
runs-on: windows-latest
@ -66,8 +69,16 @@ jobs:
- name: Print build system information
run: python3 .github/workflows/system-info.py
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
- name: Install Python dependencies
run: >
python3 -m pip install
coverage>=7.4.2
defusedxml
olefile
pyroma
pytest
pytest-cov
pytest-timeout
- name: Install dependencies
id: install

View File

@ -27,6 +27,7 @@ concurrency:
cancel-in-progress: true
env:
COVERAGE_CORE: sysmon
FORCE_COLOR: 1
jobs:

View File

@ -19,7 +19,7 @@ FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.3.0
LIBPNG_VERSION=1.6.40
JPEGTURBO_VERSION=3.0.1
OPENJPEG_VERSION=2.5.0
OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg {
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
@ -93,6 +93,9 @@ function build {
done
fi
build_openjpeg
if [ -f /usr/local/lib64/libopenjp2.so ]; then
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
fi
ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG"

View File

@ -5,6 +5,18 @@ Changelog (Pillow)
10.3.0 (unreleased)
-------------------
- Handle truncated chunks at the end of PNG images #7709
[lajiyuan, radarhere]
- Match mask size to pasted image size in GifImagePlugin #7779
[radarhere]
- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
[evanmiller, radarhere]
- Fixed reading FLI/FLC images with a prefix chunk #7804
[twolife]
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
[nik012003, radarhere]

View File

@ -82,9 +82,6 @@ As of 2019, Pillow development is
<a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img
alt="Join the chat at https://gitter.im/python-pillow/Pillow"
src="https://badges.gitter.im/python-pillow/Pillow.svg"></a>
<a href="https://twitter.com/PythonPillow"><img
alt="Follow on https://twitter.com/PythonPillow"
src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a>
<a href="https://fosstodon.org/@pillow"><img
alt="Follow on https://fosstodon.org/@pillow"
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"

View File

@ -86,7 +86,7 @@ Released as needed privately to individual vendors for critical security-related
## Publicize Release
* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
## Documentation

BIN
Tests/images/2422.flc Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -20,7 +20,7 @@ from PIL import _deprecate
),
],
)
def test_version(version, expected) -> None:
def test_version(version: int | None, expected: str) -> None:
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing")
@ -46,7 +46,7 @@ def test_unknown_version() -> None:
),
],
)
def test_old_version(deprecated, plural, expected) -> None:
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
expected = r""
with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural)
@ -76,7 +76,7 @@ def test_replacement_and_action() -> None:
"Upgrade to new thing.",
],
)
def test_action(action) -> None:
def test_action(action: str) -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Upgrade to new thing\."

View File

@ -21,9 +21,16 @@ def test_isatty() -> None:
assert container.isatty() is False
def test_seek_mode_0() -> None:
@pytest.mark.parametrize(
"mode, expected_value",
(
(0, 33),
(1, 66),
(2, 100),
),
)
def test_seek_mode(mode: int, expected_value: int) -> None:
# Arrange
mode = 0
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@ -32,35 +39,7 @@ def test_seek_mode_0() -> None:
container.seek(33, mode)
# Assert
assert container.tell() == 33
def test_seek_mode_1() -> None:
# Arrange
mode = 1
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(33, mode)
container.seek(33, mode)
# Assert
assert container.tell() == 66
def test_seek_mode_2() -> None:
# Arrange
mode = 2
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(33, mode)
container.seek(33, mode)
# Assert
assert container.tell() == 100
assert container.tell() == expected_value
@pytest.mark.parametrize("bytesmode", (True, False))

View File

@ -4,7 +4,7 @@ import warnings
import pytest
from PIL import FliImagePlugin, Image
from PIL import FliImagePlugin, Image, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
@ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# save as...-> hopper.fli, default options.
static_test_file = "Tests/images/hopper.fli"
# From https://samples.libav.org/fli-flc/
# From https://samples.ffmpeg.org/fli-flc/
animated_test_file = "Tests/images/a.fli"
# From https://samples.ffmpeg.org/fli-flc/
animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
def test_sanity() -> None:
with Image.open(static_test_file) as im:
@ -32,6 +35,24 @@ 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
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
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
def test_unclosed_file() -> None:
def open() -> None:

View File

@ -1113,6 +1113,21 @@ def test_append_images(tmp_path: Path) -> None:
assert reread.n_frames == 10
def test_append_different_size_image(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im = Image.new("RGB", (100, 100))
bigger_im = Image.new("RGB", (200, 200), "#f00")
im.save(out, save_all=True, append_images=[bigger_im])
with Image.open(out) as reread:
assert reread.size == (100, 100)
reread.seek(1)
assert reread.size == (100, 100)
def test_transparent_optimize(tmp_path: Path) -> None:
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
# transparency.

View File

@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode) -> None:
def test_save_to_bytes_bmp(mode: str) -> None:
output = io.BytesIO()
im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])

View File

@ -98,7 +98,7 @@ def test_i() -> None:
assert ret == 97
def test_dump(monkeypatch) -> None:
def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
c = b"abc"
# Temporarily redirect stdout

View File

@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
assert isinstance(im, MspImagePlugin.MspImageFile)
def _assert_file_image_equal(source_path, target_path) -> None:
def _assert_file_image_equal(source_path: str, target_path: str) -> None:
with Image.open(source_path) as im:
assert_image_equal_tofile(im, target_path)

View File

@ -6,6 +6,7 @@ import warnings
import zlib
from io import BytesIO
from pathlib import Path
from types import ModuleType
from typing import Any
import pytest
@ -23,6 +24,7 @@ from .helper import (
skip_unless_feature,
)
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
@ -781,6 +783,18 @@ class TestFilePng:
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
def test_truncated_end_chunk(self) -> 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
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib")

View File

@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None:
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
],
)
def test_crashes(test_file, raises) -> None:
def test_crashes(test_file: str, raises) -> None:
with open(test_file, "rb") as f:
with pytest.raises(raises):
with Image.open(f):

View File

@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@pytest.mark.parametrize("mode", _MODES)
def test_sanity(mode, tmp_path: Path) -> None:
def roundtrip(original_im) -> None:
def test_sanity(mode: str, tmp_path: Path) -> None:
def roundtrip(original_im: Image.Image) -> None:
out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle)

View File

@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None:
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
def test_writing_other_types_to_ascii(
value: bytes | int, expected: str, tmp_path: Path
) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
@pytest.mark.parametrize("value", (1, IFDRational(1)))
def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()

View File

@ -685,15 +685,18 @@ class TestImage:
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
def test_p_from_rgb_rgba(self) -> None:
for mode, color in [
@pytest.mark.parametrize(
"mode, color",
(
("RGB", "#DDEEFF"),
("RGB", (221, 238, 255)),
("RGBA", (221, 238, 255, 255)),
]:
im = Image.new("P", (100, 100), color)
expected = Image.new(mode, (100, 100), color)
assert_image_equal(im.convert(mode), expected)
),
)
def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None:
im = Image.new("P", (100, 100), color)
expected = Image.new(mode, (100, 100), color)
assert_image_equal(im.convert(mode), expected)
def test_no_resource_warning_on_save(self, tmp_path: Path) -> None:
# https://github.com/python-pillow/Pillow/issues/835

View File

@ -6,6 +6,7 @@ import re
import shutil
from io import BytesIO
from pathlib import Path
from typing import Any
import pytest
@ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None:
@pytest.mark.parametrize("flag", ("my string", -1))
def test_invalid_flag(flag) -> None:
def test_invalid_flag(flag: str | int) -> None:
with hopper() as im:
with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
@ -335,19 +336,21 @@ def test_extended_information() -> None:
o = ImageCms.getOpenProfile(SRGB)
p = o.profile
def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None:
def assert_truncated_tuple_equal(
tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10
) -> None:
# Helper function to reduce precision of tuples of floats
# recursively and then check equality.
power = 10**digits
def truncate_tuple(tuple_or_float):
def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]:
return tuple(
(
truncate_tuple(val)
if isinstance(val, tuple)
else int(val * power) / power
)
for val in tuple_or_float
for val in tuple_value
)
assert truncate_tuple(tup1) == truncate_tuple(tup2)
@ -504,8 +507,10 @@ def test_profile_typesafety() -> None:
ImageCms.ImageCmsProfile(1).tobytes()
def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None:
def create_test_image():
def assert_aux_channel_preserved(
mode: str, transform_in_place: bool, preserved_channel: str
) -> None:
def create_test_image() -> Image.Image:
# set up test image with something interesting in the tested aux channel.
# fmt: off
nine_grid_deltas = [
@ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
def test_rgb_lab(mode) -> None:
def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (0, 128, 128)

View File

@ -7,7 +7,7 @@ import shutil
import sys
from io import BytesIO
from pathlib import Path
from typing import BinaryIO
from typing import Any, BinaryIO
import pytest
from packaging.version import parse as parse_version
@ -44,7 +44,7 @@ def test_sanity() -> None:
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
],
)
def layout_engine(request):
def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param
@ -535,21 +535,23 @@ def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
)
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
def test_find_font(monkeypatch, platform, font_directory) -> None:
def test_find_font(
monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str
) -> None:
def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None:
# Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with monkeypatch.context() as m:
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
def loadable_font(filepath, size, index, encoding, *args, **kwargs):
def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any
):
if filepath == path_to_fake:
return ImageFont._FreeTypeFont(
FONT_PATH, size, index, encoding, *args, **kwargs
FONT_PATH, size, index, encoding, *args
)
return ImageFont._FreeTypeFont(
filepath, size, index, encoding, *args, **kwargs
)
return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname)
@ -563,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
if platform == "linux":
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
def fake_walker(path):
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
if path == font_directory:
return [
(
@ -1101,7 +1103,7 @@ def test_oom(test_file: str) -> None:
font.getmask("Test Text")
def test_raqm_missing_warning(monkeypatch) -> None:
def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record:
font = ImageFont.truetype(

View File

@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
assert p.stdin is not None
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
p.communicate()
@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_png(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
assert p.stdin is not None
p.stdin.write(
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
$ms = new-object System.IO.MemoryStream(, $bytes)
@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only",
)
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
def test_grabclipboard_wl_clipboard(self, ext) -> None:
def test_grabclipboard_wl_clipboard(self, ext: str) -> None:
image_path = "Tests/images/hopper." + ext
with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp)
@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only",
)
@pytest.mark.parametrize("arg", ("text", "--clear"))
def test_grabclipboard_wl_clipboard_errors(self, arg):
def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None:
subprocess.call(["wl-copy", arg])
assert ImageGrab.grabclipboard() is None

View File

@ -58,7 +58,9 @@ def test_path() -> None:
ImagePath.Path((0, 1)),
),
)
def test_path_constructors(coords) -> None:
def test_path_constructors(
coords: Sequence[float] | array.array[float] | ImagePath.Path,
) -> None:
# Arrange / Act
p = ImagePath.Path(coords)
@ -206,9 +208,9 @@ class Evil:
def __init__(self) -> None:
self.corrupt = Image.core.path(0x4000000000000000)
def __getitem__(self, i):
def __getitem__(self, i: int) -> bytes:
x = self.corrupt[i]
return struct.pack("dd", x[0], x[1])
def __setitem__(self, i, x) -> None:
def __setitem__(self, i: int, x: bytes) -> None:
self.corrupt[i] = struct.unpack("dd", x)

View File

@ -28,7 +28,7 @@ def test_rgb() -> None:
assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255)
def checkrgb(r, g, b) -> None:
def checkrgb(r: int, g: int, b: int) -> None:
val = ImageQt.rgb(r, g, b)
val = val % 2**24 # drop the alpha
assert val >> 16 == r

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from typing import Any
import pytest
from PIL import Image, ImageShow
@ -24,9 +26,9 @@ def test_register() -> None:
"order",
[-1, 0],
)
def test_viewer_show(order) -> None:
def test_viewer_show(order: int) -> None:
class TestViewer(ImageShow.Viewer):
def show_image(self, image, **options) -> bool:
def show_image(self, image: Image.Image, **options: Any) -> bool:
self.methodCalled = True
return True
@ -48,7 +50,7 @@ def test_viewer_show(order) -> None:
reason="Only run on CIs; hangs on Windows CIs",
)
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
def test_show(mode) -> None:
def test_show(mode: str) -> None:
im = hopper(mode)
assert ImageShow.show(im)
@ -73,7 +75,7 @@ def test_viewer() -> None:
@pytest.mark.parametrize("viewer", ImageShow._viewers)
def test_viewers(viewer) -> None:
def test_viewers(viewer: ImageShow.Viewer) -> None:
try:
viewer.get_command("test.jpg")
except NotImplementedError:

View File

@ -70,7 +70,7 @@ if is_win32():
]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
def serialize_dib(bi, pixels):
def serialize_dib(bi, pixels) -> bytearray:
bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize

View File

@ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None:
def to_image(dtype, bands: int = 1, boolean: int = 0):
def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image:
if bands == 1:
if boolean:
data = [0, 255] * 50
@ -99,7 +99,7 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5))
def _test_img_equals_nparray(img, np) -> None:
def _test_img_equals_nparray(img: Image.Image, np) -> None:
assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0]
assert img.size == np_size
@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8),
),
)
def test_to_array(mode, dtype) -> None:
def test_to_array(mode: str, dtype) -> None:
img = hopper(mode)
# Resize to non-square

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from PIL import ImageQt
from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -37,7 +37,7 @@ if ImageQt.qt_is_installed:
lbl.setPixmap(pixmap1.copy())
def roundtrip(expected) -> None:
def roundtrip(expected: Image.Image) -> None:
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb
assert_image_similar(result, expected.convert("RGB"), 1)

View File

@ -17,7 +17,7 @@ if ImageQt.qt_is_installed:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
def test_sanity(mode, tmp_path: Path) -> None:
def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)

View File

@ -10,7 +10,7 @@ from PIL import _util
@pytest.mark.parametrize(
"test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")]
)
def test_is_path(test_path) -> None:
def test_is_path(test_path: str | Path | PurePath) -> None:
# Act
it_is = _util.is_path(test_path)

View File

@ -1,7 +1,7 @@
#!/bin/bash
# install openjpeg
archive=openjpeg-2.5.0
archive=openjpeg-2.5.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -504,3 +504,27 @@ PIL.OleFileIO
the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
PyPI (eg. ``python3 -m pip install olefile``).
import _imaging
~~~~~~~~~~~~~~~
.. versionremoved:: 2.1.0
Pillow >= 2.1.0 no longer supports ``import _imaging``.
Please use ``from PIL.Image import core as _imaging`` instead.
Pillow and PIL
~~~~~~~~~~~~~~
.. versionremoved:: 1.0.0
Pillow and PIL cannot co-exist in the same environment.
Before installing Pillow, please uninstall PIL.
import Image
~~~~~~~~~~~~
.. versionremoved:: 1.0.0
Pillow >= 1.0 no longer supports ``import Image``.
Please use ``from PIL import Image`` instead.

View File

@ -73,10 +73,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
:alt: Join the chat at https://gitter.im/python-pillow/Pillow
.. image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg
:target: https://twitter.com/PythonPillow
:alt: Follow on https://twitter.com/PythonPillow
.. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg
:target: https://fosstodon.org/@pillow
:alt: Follow on https://fosstodon.org/@pillow

View File

@ -9,15 +9,6 @@ Installation
});
</script>
Warnings
--------
.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL.
.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead.
.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead.
Python Support
--------------
@ -186,7 +177,7 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
**2.4.0** and **2.5.0**.
**2.4.0**, **2.5.0** and **2.5.2**.
* Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie.

View File

@ -14,6 +14,8 @@ only work on L and RGB images.
.. autofunction:: colorize
.. autofunction:: crop
.. autofunction:: scale
.. autoclass:: SupportsGetMesh
:show-inheritance:
.. autofunction:: deform
.. autofunction:: equalize
.. autofunction:: expand

View File

@ -79,3 +79,9 @@ Portable FloatMap (PFM) images
Support has been added for reading and writing grayscale (Pf format)
Portable FloatMap (PFM) files containing ``F`` data.
Release GIL when fetching WebP frames
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Python's Global Interpreter Lock is now released when fetching WebP frames from
the libwebp decoder.

View File

@ -33,6 +33,7 @@ classifiers = [
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
"Topic :: Multimedia :: Graphics :: Viewers",
"Typing :: Typed",
]
dynamic = [
"version",
@ -79,7 +80,6 @@ Homepage = "https://python-pillow.org"
Mastodon = "https://fosstodon.org/@pillow"
"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
Source = "https://github.com/python-pillow/Pillow"
Twitter = "https://twitter.com/PythonPillow"
[tool.setuptools]
packages = ["PIL"]

View File

@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF100:
# prefix chunk; ignore it
self.__offset = self.__offset + i32(s)
self.fp.seek(self.__offset)
s = self.fp.read(16)
if i16(s, 4) == 0xF1FA:

View File

@ -649,9 +649,7 @@ def _write_multiple_frames(im, fp, palette):
if "transparency" in encoderinfo:
# When the delta is zero, fill the image with transparency
diff_frame = im_frame.copy()
fill = Image.new(
"P", diff_frame.size, encoderinfo["transparency"]
)
fill = Image.new("P", delta.size, encoderinfo["transparency"])
if delta.mode == "RGBA":
r, g, b, a = delta.split()
mask = ImageMath.eval(

View File

@ -411,7 +411,15 @@ def scale(
return image.resize(size, resample)
class _SupportsGetMesh(Protocol):
class SupportsGetMesh(Protocol):
"""
An object that supports the ``getmesh`` method, taking an image as an
argument, and returning a list of tuples. Each tuple contains two tuples,
the source box as a tuple of 4 integers, and a tuple of 8 integers for the
final quadrilateral, in order of top left, bottom left, bottom right, top
right.
"""
def getmesh(
self, image: Image.Image
) -> list[
@ -421,7 +429,7 @@ class _SupportsGetMesh(Protocol):
def deform(
image: Image.Image,
deformer: _SupportsGetMesh,
deformer: SupportsGetMesh,
resample: int = Image.Resampling.BILINEAR,
) -> Image.Image:
"""

View File

@ -981,7 +981,13 @@ class PngImageFile(ImageFile.ImageFile):
except EOFError:
if cid == b"fdAT":
length -= 4
ImageFile._safe_read(self.fp, length)
try:
ImageFile._safe_read(self.fp, length)
except OSError as e:
if ImageFile.LOAD_TRUNCATED_IMAGES:
break
else:
raise e
except AttributeError:
logger.debug("%r %s %s (unknown)", cid, pos, length)
s = ImageFile._safe_read(self.fp, length)

0
src/PIL/py.typed Normal file
View File

View File

@ -448,11 +448,16 @@ PyObject *
_anim_decoder_get_next(PyObject *self) {
uint8_t *buf;
int timestamp;
int ok;
PyObject *bytes;
PyObject *ret;
ImagingSectionCookie cookie;
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
if (!WebPAnimDecoderGetNext(decp->dec, &buf, &timestamp)) {
ImagingSectionEnter(&cookie);
ok = WebPAnimDecoderGetNext(decp->dec, &buf, &timestamp);
ImagingSectionLeave(&cookie);
if (!ok) {
PyErr_SetString(PyExc_OSError, "failed to read next frame");
return NULL;
}

View File

@ -308,21 +308,16 @@ DEPS = {
"libs": [r"Lib\MS\*.lib"],
},
"openjpeg": {
"url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz",
"filename": "openjpeg-2.5.0.tar.gz",
"dir": "openjpeg-2.5.0",
"url": "https://github.com/uclouvain/openjpeg/archive/v2.5.2.tar.gz",
"filename": "openjpeg-2.5.2.tar.gz",
"dir": "openjpeg-2.5.2",
"license": "LICENSE",
"patch": {
r"src\lib\openjp2\ht_dec.c": {
"#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501
}
},
"build": [
*cmds_cmake(
"openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF"
),
cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"),
cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"),
cmd_mkdir(r"{inc_dir}\openjpeg-2.5.2"),
cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.2"),
],
"libs": [r"bin\*.lib"],
},