mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-10 19:56:47 +03:00
Merge branch 'main' into bugreport
This commit is contained in:
commit
01fdf2ff51
|
@ -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
|
||||
|
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
|
@ -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:
|
||||
|
|
3
.github/workflows/test-cygwin.yml
vendored
3
.github/workflows/test-cygwin.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
|
5
.github/workflows/test-mingw.yml
vendored
5
.github/workflows/test-mingw.yml
vendored
|
@ -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
|
||||
|
|
15
.github/workflows/test-windows.yml
vendored
15
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||
|
|
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
@ -27,6 +27,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
|
|
7
.github/workflows/wheels-dependencies.sh
vendored
7
.github/workflows/wheels-dependencies.sh
vendored
|
@ -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"
|
||||
|
|
12
CHANGES.rst
12
CHANGES.rst
|
@ -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]
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
BIN
Tests/images/2422.flc
Normal file
Binary file not shown.
BIN
Tests/images/truncated_end_chunk.png
Normal file
BIN
Tests/images/truncated_end_chunk.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -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\."
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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
0
src/PIL/py.typed
Normal 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, ×tamp)) {
|
||||
ImagingSectionEnter(&cookie);
|
||||
ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp);
|
||||
ImagingSectionLeave(&cookie);
|
||||
if (!ok) {
|
||||
PyErr_SetString(PyExc_OSError, "failed to read next frame");
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
@ -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"],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user