Merge branch 'python-pillow:main' into main

This commit is contained in:
Sascha Ronnie Daoudia 2024-01-26 17:41:31 +01:00 committed by GitHub
commit 747453764d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
186 changed files with 426 additions and 158 deletions

View File

@ -37,16 +37,26 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip
cache-dependency-path: ".ci/*.sh" cache-dependency-path: |
".ci/*.sh"
"pyproject.toml"
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: Cache libimagequant
uses: actions/cache@v4
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Install Linux dependencies - name: Install Linux dependencies
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: "3.x" GHA_PYTHON_VERSION: "3.x"
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Build - name: Build
run: | run: |

View File

@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pre-commit path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}

View File

@ -95,7 +95,7 @@ jobs:
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
- name: pip cache - name: pip cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: 'C:\cygwin\home\runneradmin\.cache\pip' path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}

View File

@ -89,7 +89,7 @@ jobs:
- name: Cache build - name: Cache build
id: build-cache id: build-cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: winbuild\build path: winbuild\build
key: key:

View File

@ -26,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
@ -65,17 +68,28 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
cache: pip cache: pip
cache-dependency-path: ".ci/*.sh" cache-dependency-path: |
".ci/*.sh"
"pyproject.toml"
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v4
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Install Linux dependencies - name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu') if: startsWith(matrix.os, 'ubuntu')
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Install macOS dependencies - name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
10.3.0 (unreleased) 10.3.0 (unreleased)
------------------- -------------------
- Do not support using test-image-results to upload images after test failures #7739
[radarhere]
- Changed ImageMath.ops to be static #7721
[radarhere]
- Fix APNG info after seeking backwards more than twice #7701 - Fix APNG info after seeking backwards more than twice #7701
[esoma, radarhere] [esoma, radarhere]

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import time import time
from PIL import PyAccess from PIL import PyAccess

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image
TEST_FILE = "Tests/images/fli_overflow.fli" TEST_FILE = "Tests/images/fli_overflow.fli"
def test_fli_overflow(): def test_fli_overflow() -> None:
# this should not crash with a malloc error or access violation # this should not crash with a malloc error or access violation
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()

View File

@ -1,5 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable
import pytest import pytest
from PIL import Image from PIL import Image
@ -12,31 +15,34 @@ max_iterations = 10000
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
def _get_mem_usage(): def _get_mem_usage() -> float:
from resource import RUSAGE_SELF, getpagesize, getrusage from resource import RUSAGE_SELF, getpagesize, getrusage
mem = getrusage(RUSAGE_SELF).ru_maxrss mem = getrusage(RUSAGE_SELF).ru_maxrss
return mem * getpagesize() / 1024 / 1024 return mem * getpagesize() / 1024 / 1024
def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs): def _test_leak(
min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
) -> None:
mem_limit = None mem_limit = None
for i in range(max_iterations): for i in range(max_iterations):
fn(*args, **kwargs) fn(*args)
mem = _get_mem_usage() mem = _get_mem_usage()
if i < min_iterations: if i < min_iterations:
mem_limit = mem + 1 mem_limit = mem + 1
continue continue
msg = f"memory usage limit exceeded after {i + 1} iterations" msg = f"memory usage limit exceeded after {i + 1} iterations"
assert mem_limit is not None
assert mem <= mem_limit, msg assert mem <= mem_limit, msg
def test_leak_putdata(): def test_leak_putdata() -> None:
im = Image.new("RGB", (25, 25)) im = Image.new("RGB", (25, 25))
_test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) _test_leak(min_iterations, max_iterations, im.putdata, im.getdata())
def test_leak_getlist(): def test_leak_getlist() -> None:
im = Image.new("P", (25, 25)) im = Image.new("P", (25, 25))
_test_leak( _test_leak(
min_iterations, min_iterations,

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -19,7 +20,7 @@ pytestmark = [
] ]
def test_leak_load(): def test_leak_load() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_STACK, (stack_size, stack_size))
@ -29,7 +30,7 @@ def test_leak_load():
im.load() im.load()
def test_leak_save(): def test_leak_save() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_STACK, (stack_size, stack_size))

View File

@ -1,10 +1,13 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image from PIL import Image
def test_j2k_overflow(tmp_path): def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584)) im = Image.new("RGBA", (1024, 131584))
target = str(tmp_path / "temp.jpc") target = str(tmp_path / "temp.jpc")
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -14,7 +14,6 @@
# version. # version.
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -110,14 +111,14 @@ standard_chrominance_qtable = (
[standard_l_qtable, standard_chrominance_qtable], [standard_l_qtable, standard_chrominance_qtable],
), ),
) )
def test_qtables_leak(qtables): def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
im = hopper("RGB") im = hopper("RGB")
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables) im.save(test_output, "JPEG", qtables=qtables)
def test_exif_leak(): def test_exif_leak() -> None:
""" """
pre patch: pre patch:
@ -180,7 +181,7 @@ def test_exif_leak():
im.save(test_output, "JPEG", exif=exif) im.save(test_output, "JPEG", exif=exif)
def test_base_save(): def test_base_save() -> None:
""" """
base case: base case:
MB MB

View File

@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from pathlib import Path
from types import ModuleType
import pytest import pytest
@ -15,6 +18,7 @@ from PIL import Image
# 2.7 and 3.2. # 2.7 and 3.2.
numpy: ModuleType | None
try: try:
import numpy import numpy
except ImportError: except ImportError:
@ -27,23 +31,24 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
im = Image.new("L", (xdim, ydim), 0) im = Image.new("L", (xdim, ydim), 0)
im.save(f) im.save(f)
def test_large(tmp_path): def test_large(tmp_path: Path) -> None:
"""succeeded prepatch""" """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM) _write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path): def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch""" """failed prepatch"""
_write_png(tmp_path, XDIM, XDIM) _write_png(tmp_path, XDIM, XDIM)
@pytest.mark.skipif(numpy is None, reason="Numpy is not installed") @pytest.mark.skipif(numpy is None, reason="Numpy is not installed")
def test_size_greater_than_int(): def test_size_greater_than_int() -> None:
assert numpy is not None
arr = numpy.ndarray(shape=(16394, 16394)) arr = numpy.ndarray(shape=(16394, 16394))
Image.fromarray(arr) Image.fromarray(arr)

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from pathlib import Path
import pytest import pytest
@ -23,7 +25,7 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
dtype = np.uint8 dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype) a = np.zeros((xdim, ydim), dtype=dtype)
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
@ -31,11 +33,11 @@ def _write_png(tmp_path, xdim, ydim):
im.save(f) im.save(f)
def test_large(tmp_path): def test_large(tmp_path: Path) -> None:
"""succeeded prepatch""" """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM) _write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path): def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch""" """failed prepatch"""
_write_png(tmp_path, XDIM, XDIM) _write_png(tmp_path, XDIM, XDIM)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image
@ -6,7 +7,7 @@ from PIL import Image
TEST_FILE = "Tests/images/libtiff_segfault.tif" TEST_FILE = "Tests/images/libtiff_segfault.tif"
def test_libtiff_segfault(): def test_libtiff_segfault() -> None:
"""This test should not segfault. It will on Pillow <= 3.1.0 and """This test should not segfault. It will on Pillow <= 3.1.0 and
libtiff >= 4.0.0 libtiff >= 4.0.0
""" """

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import zlib import zlib
from io import BytesIO from io import BytesIO
@ -7,7 +8,7 @@ from PIL import Image, ImageFile, PngImagePlugin
TEST_FILE = "Tests/images/png_decompression_dos.png" TEST_FILE = "Tests/images/png_decompression_dos.png"
def test_ignore_dos_text(): def test_ignore_dos_text() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
try: try:
@ -23,7 +24,7 @@ def test_ignore_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_text(): def test_dos_text() -> None:
try: try:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
@ -35,7 +36,7 @@ def test_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_total_memory(): def test_dos_total_memory() -> None:
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
compressed_data = zlib.compress(b"a" * 1024 * 1023) compressed_data = zlib.compress(b"a" * 1024 * 1023)
@ -52,7 +53,7 @@ def test_dos_total_memory():
try: try:
im2 = Image.open(b) im2 = Image.open(b)
except ValueError as msg: except ValueError as msg:
assert "Too much memory" in msg assert "Too much memory" in str(msg)
return return
total_len = 0 total_len = 0

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from pathlib import Path from pathlib import Path

View File

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from PIL import features from PIL import features
def test_wheel_modules(): def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
# tkinter is not available in cibuildwheel installed CPython on Windows # tkinter is not available in cibuildwheel installed CPython on Windows
@ -18,13 +19,13 @@ def test_wheel_modules():
assert set(features.get_supported_modules()) == expected_modules assert set(features.get_supported_modules()) == expected_modules
def test_wheel_codecs(): def test_wheel_codecs() -> None:
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
assert set(features.get_supported_codecs()) == expected_codecs assert set(features.get_supported_codecs()) == expected_codecs
def test_wheel_features(): def test_wheel_features() -> None:
expected_features = { expected_features = {
"webp_anim", "webp_anim",
"webp_mux", "webp_mux",

View File

@ -1,8 +1,11 @@
from __future__ import annotations from __future__ import annotations
import io import io
import pytest
def pytest_report_header(config):
def pytest_report_header(config: pytest.Config) -> str:
try: try:
from PIL import features from PIL import features
@ -13,7 +16,7 @@ def pytest_report_header(config):
return f"pytest_report_header failed: {e}" return f"pytest_report_header failed: {e}"
def pytest_configure(config): def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line( config.addinivalue_line(
"markers", "markers",
"pil_noop_mark: A conditional mark where nothing special happens", "pil_noop_mark: A conditional mark where nothing special happens",

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import base64 import base64
import os import os

View File

@ -11,6 +11,7 @@ import sys
import sysconfig import sysconfig
import tempfile import tempfile
from io import BytesIO from io import BytesIO
from typing import Any, Callable, Sequence
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -19,42 +20,31 @@ from PIL import Image, ImageMath, features
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
uploader = None
HAS_UPLOADER = False
if os.environ.get("SHOW_ERRORS"): if os.environ.get("SHOW_ERRORS"):
# local img.show for errors. uploader = "show"
HAS_UPLOADER = True elif "GITHUB_ACTIONS" in os.environ:
uploader = "github_actions"
class test_image_results:
@staticmethod def upload(a: Image.Image, b: Image.Image) -> str | None:
def upload(a, b): if uploader == "show":
# local img.show for errors.
a.show() a.show()
b.show() b.show()
elif uploader == "github_actions":
elif "GITHUB_ACTIONS" in os.environ:
HAS_UPLOADER = True
class test_image_results:
@staticmethod
def upload(a, b):
dir_errors = os.path.join(os.path.dirname(__file__), "errors") dir_errors = os.path.join(os.path.dirname(__file__), "errors")
os.makedirs(dir_errors, exist_ok=True) os.makedirs(dir_errors, exist_ok=True)
tmpdir = tempfile.mkdtemp(dir=dir_errors) tmpdir = tempfile.mkdtemp(dir=dir_errors)
a.save(os.path.join(tmpdir, "a.png")) a.save(os.path.join(tmpdir, "a.png"))
b.save(os.path.join(tmpdir, "b.png")) b.save(os.path.join(tmpdir, "b.png"))
return tmpdir return tmpdir
return None
else:
try:
import test_image_results
HAS_UPLOADER = True
except ImportError:
pass
def convert_to_comparable(a, b): def convert_to_comparable(
a: Image.Image, b: Image.Image
) -> tuple[Image.Image, Image.Image]:
new_a, new_b = a, b new_a, new_b = a, b
if a.mode == "P": if a.mode == "P":
new_a = Image.new("L", a.size) new_a = Image.new("L", a.size)
@ -67,14 +57,18 @@ def convert_to_comparable(a, b):
return new_a, new_b return new_a, new_b
def assert_deep_equal(a, b, msg=None): def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
try: try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception: except Exception:
assert a == b, msg assert a == b, msg
def assert_image(im, mode, size, msg=None): def assert_image(
im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None
) -> None:
if mode is not None: if mode is not None:
assert im.mode == mode, ( assert im.mode == mode, (
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
@ -86,13 +80,13 @@ def assert_image(im, mode, size, msg=None):
) )
def assert_image_equal(a, b, msg=None): def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
if a.tobytes() != b.tobytes(): if a.tobytes() != b.tobytes():
if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = upload(a, b)
if url:
logger.error("URL for test images: %s", url) logger.error("URL for test images: %s", url)
except Exception: except Exception:
pass pass
@ -100,14 +94,18 @@ def assert_image_equal(a, b, msg=None):
pytest.fail(msg or "got different content") pytest.fail(msg or "got different content")
def assert_image_equal_tofile(a, filename, msg=None, mode=None): def assert_image_equal_tofile(
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode: if mode:
img = img.convert(mode) img = img.convert(mode)
assert_image_equal(a, img, msg) assert_image_equal(a, img, msg)
def assert_image_similar(a, b, epsilon, msg=None): def assert_image_similar(
a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None
) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
@ -125,55 +123,68 @@ def assert_image_similar(a, b, epsilon, msg=None):
+ f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
) )
except Exception as e: except Exception as e:
if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = upload(a, b)
if url:
logger.exception("URL for test images: %s", url) logger.exception("URL for test images: %s", url)
except Exception: except Exception:
pass pass
raise e raise e
def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None): def assert_image_similar_tofile(
a: Image.Image,
filename: str,
epsilon: float,
msg: str | None = None,
mode: str | None = None,
) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode: if mode:
img = img.convert(mode) img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg) assert_image_similar(a, img, epsilon, msg)
def assert_all_same(items, msg=None): def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg assert items.count(items[0]) == len(items), msg
def assert_not_all_same(items, msg=None): def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg assert items.count(items[0]) != len(items), msg
def assert_tuple_approx_equal(actuals, targets, threshold, msg): def assert_tuple_approx_equal(
actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str
) -> None:
"""Tests if actuals has values within threshold from targets""" """Tests if actuals has values within threshold from targets"""
value = True
for i, target in enumerate(targets): for i, target in enumerate(targets):
value *= target - threshold <= actuals[i] <= target + threshold if not (target - threshold <= actuals[i] <= target + threshold):
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
assert value, msg + ": " + repr(actuals) + " != " + repr(targets)
def skip_unless_feature(feature): def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available" reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)
def skip_unless_feature_version(feature, version_required, reason=None): def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator:
if not features.check(feature): if not features.check(feature):
return pytest.mark.skip(f"{feature} not available") return pytest.mark.skip(f"{feature} not available")
if reason is None: if reason is None:
reason = f"{feature} is older than {version_required}" reason = f"{feature} is older than {required}"
version_required = parse_version(version_required) version_required = parse_version(required)
version_available = parse_version(features.version(feature)) version_available = parse_version(features.version(feature))
return pytest.mark.skipif(version_available < version_required, reason=reason) return pytest.mark.skipif(version_available < version_required, reason=reason)
def mark_if_feature_version(mark, feature, version_blacklist, reason=None): def mark_if_feature_version(
mark: pytest.MarkDecorator,
feature: str,
version_blacklist: str,
reason: str | None = None,
) -> pytest.MarkDecorator:
if not features.check(feature): if not features.check(feature):
return pytest.mark.pil_noop_mark() return pytest.mark.pil_noop_mark()
if reason is None: if reason is None:
@ -194,7 +205,7 @@ class PillowLeakTestCase:
iterations = 100 # count iterations = 100 # count
mem_limit = 512 # k mem_limit = 512 # k
def _get_mem_usage(self): def _get_mem_usage(self) -> float:
""" """
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
between macOS and Linux rss reporting between macOS and Linux rss reporting
@ -216,7 +227,7 @@ class PillowLeakTestCase:
# This is the maximum resident set size used (in kilobytes). # This is the maximum resident set size used (in kilobytes).
return mem # Kb return mem # Kb
def _test_leak(self, core): def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage() start_mem = self._get_mem_usage()
for cycle in range(self.iterations): for cycle in range(self.iterations):
core() core()
@ -228,17 +239,17 @@ class PillowLeakTestCase:
# helpers # helpers
def fromstring(data): def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))
def tostring(im, string_format, **options): def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
out = BytesIO() out = BytesIO()
im.save(out, string_format, **options) im.save(out, string_format, **options)
return out.getvalue() return out.getvalue()
def hopper(mode=None, cache={}): def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
if mode is None: if mode is None:
# Always return fresh not-yet-loaded version of image. # Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors # Operations on not-yet-loaded images is separate class of errors
@ -259,29 +270,31 @@ def hopper(mode=None, cache={}):
return im.copy() return im.copy()
def djpeg_available(): def djpeg_available() -> bool:
if shutil.which("djpeg"): if shutil.which("djpeg"):
try: try:
subprocess.check_call(["djpeg", "-version"]) subprocess.check_call(["djpeg", "-version"])
return True return True
except subprocess.CalledProcessError: # pragma: no cover except subprocess.CalledProcessError: # pragma: no cover
return False return False
return False
def cjpeg_available(): def cjpeg_available() -> bool:
if shutil.which("cjpeg"): if shutil.which("cjpeg"):
try: try:
subprocess.check_call(["cjpeg", "-version"]) subprocess.check_call(["cjpeg", "-version"])
return True return True
except subprocess.CalledProcessError: # pragma: no cover except subprocess.CalledProcessError: # pragma: no cover
return False return False
return False
def netpbm_available(): def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
def magick_command(): def magick_command() -> list[str] | None:
if sys.platform == "win32": if sys.platform == "win32":
magickhome = os.environ.get("MAGICK_HOME") magickhome = os.environ.get("MAGICK_HOME")
if magickhome: if magickhome:
@ -298,47 +311,48 @@ def magick_command():
return imagemagick return imagemagick
if graphicsmagick and shutil.which(graphicsmagick[0]): if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick return graphicsmagick
return None
def on_appveyor(): def on_appveyor() -> bool:
return "APPVEYOR" in os.environ return "APPVEYOR" in os.environ
def on_github_actions(): def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ return "GITHUB_ACTIONS" in os.environ
def on_ci(): def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI" # GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ return "CI" in os.environ
def is_big_endian(): def is_big_endian() -> bool:
return sys.byteorder == "big" return sys.byteorder == "big"
def is_ppc64le(): def is_ppc64le() -> bool:
import platform import platform
return platform.machine() == "ppc64le" return platform.machine() == "ppc64le"
def is_win32(): def is_win32() -> bool:
return sys.platform.startswith("win32") return sys.platform.startswith("win32")
def is_pypy(): def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info") return hasattr(sys, "pypy_translation_info")
def is_mingw(): def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw" return sysconfig.get_platform() == "mingw"
class CachedProperty: class CachedProperty:
def __init__(self, func): def __init__(self, func: Callable[[Any], None]) -> None:
self.func = func self.func = func
def __get__(self, instance, cls=None): def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
result = instance.__dict__[self.func.__name__] = self.func(instance) result = instance.__dict__[self.func.__name__] = self.func(instance)
return result return result

View File

@ -23,7 +23,7 @@ with atheris.instrument_imports():
import fuzzers import fuzzers
def TestOneInput(data): def TestOneInput(data: bytes) -> None:
try: try:
fuzzers.fuzz_font(data) fuzzers.fuzz_font(data)
except Exception: except Exception:
@ -32,7 +32,7 @@ def TestOneInput(data):
pass pass
def main(): def main() -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()

View File

@ -23,7 +23,7 @@ with atheris.instrument_imports():
import fuzzers import fuzzers
def TestOneInput(data): def TestOneInput(data: bytes) -> None:
try: try:
fuzzers.fuzz_image(data) fuzzers.fuzz_image(data)
except Exception: except Exception:
@ -32,7 +32,7 @@ def TestOneInput(data):
pass pass
def main(): def main() -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()

View File

@ -1,22 +1,23 @@
from __future__ import annotations from __future__ import annotations
import io import io
import warnings import warnings
from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont
def enable_decompressionbomb_error(): def enable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
warnings.simplefilter("error", Image.DecompressionBombWarning) warnings.simplefilter("error", Image.DecompressionBombWarning)
def disable_decompressionbomb_error(): def disable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
warnings.resetwarnings() warnings.resetwarnings()
def fuzz_image(data): def fuzz_image(data: bytes) -> None:
# This will fail on some images in the corpus, as we have many # This will fail on some images in the corpus, as we have many
# invalid images in the test suite. # invalid images in the test suite.
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
@ -25,7 +26,7 @@ def fuzz_image(data):
im.save(io.BytesIO(), "BMP") im.save(io.BytesIO(), "BMP")
def fuzz_font(data): def fuzz_font(data: bytes) -> None:
wrapper = io.BytesIO(data) wrapper = io.BytesIO(data)
try: try:
font = ImageFont.truetype(wrapper) font = ImageFont.truetype(wrapper)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import subprocess import subprocess
import sys import sys
@ -23,7 +24,7 @@ if features.check("libjpeg_turbo"):
"path", "path",
subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"),
) )
def test_fuzz_images(path): def test_fuzz_images(path: str) -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
try: try:
with open(path, "rb") as f: with open(path, "rb") as f:
@ -54,7 +55,7 @@ def test_fuzz_images(path):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
) )
def test_fuzz_fonts(path): def test_fuzz_fonts(path: str) -> None:
if not path: if not path:
return return
with open(path, "rb") as f: with open(path, "rb") as f:

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from PIL import _binary from PIL import _binary

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import warnings import warnings

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, ImageFilter from PIL import Image, ImageFilter

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from array import array from array import array
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import _deprecate from PIL import _deprecate

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import re import re

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, ImageSequence, PngImagePlugin from PIL import Image, ImageSequence, PngImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import BufrStubImagePlugin, Image from PIL import BufrStubImagePlugin, Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import ContainerIO, Image from PIL import ContainerIO, Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import CurImagePlugin, Image from PIL import CurImagePlugin, Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest

View File

@ -1,5 +1,6 @@
"""Test DdsImagePlugin""" """Test DdsImagePlugin"""
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import FtexImagePlugin, Image from PIL import FtexImagePlugin, Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import GbrImagePlugin, Image from PIL import GbrImagePlugin, Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import GdImageFile, UnidentifiedImageError from PIL import GdImageFile, UnidentifiedImageError

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from PIL import GimpGradientFile, ImagePalette from PIL import GimpGradientFile, ImagePalette

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL.GimpPaletteFile import GimpPaletteFile from PIL.GimpPaletteFile import GimpPaletteFile

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import GribStubImagePlugin, Image from PIL import GribStubImagePlugin, Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Hdf5StubImagePlugin, Image from PIL import Hdf5StubImagePlugin, Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import os import os
import warnings import warnings

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import os import os

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import filecmp import filecmp
import warnings import warnings

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from io import BytesIO, StringIO from io import BytesIO, StringIO

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re import re
import warnings import warnings

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re import re
from io import BytesIO from io import BytesIO

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import base64 import base64
import io import io
import itertools import itertools

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, McIdasImagePlugin from PIL import Image, McIdasImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, ImagePalette from PIL import Image, ImagePalette

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os.path import os.path
import subprocess import subprocess

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, ImageFile, PcxImagePlugin from PIL import Image, ImageFile, PcxImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import os import os
import os.path import os.path

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, PixarImagePlugin from PIL import Image, PixarImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import re import re
import sys import sys
import warnings import warnings

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from io import BytesIO from io import BytesIO

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, QoiImagePlugin from PIL import Image, QoiImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, SgiImagePlugin from PIL import Image, SgiImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import tempfile import tempfile
import warnings import warnings
from io import BytesIO from io import BytesIO

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
from glob import glob from glob import glob
from itertools import product from itertools import product

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os import os
import warnings import warnings
from io import BytesIO from io import BytesIO

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import struct import struct

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from PIL import WalImageFile from PIL import WalImageFile
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import io import io
import re import re
import sys import sys

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, WmfImagePlugin from PIL import Image, WmfImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, XpmImagePlugin from PIL import Image, XpmImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, XVThumbImagePlugin from PIL import Image, XVThumbImagePlugin

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import BdfFontFile, FontFile from PIL import BdfFontFile, FontFile
@ -6,7 +7,7 @@ from PIL import BdfFontFile, FontFile
filename = "Tests/images/courB08.bdf" filename = "Tests/images/courB08.bdf"
def test_sanity(): def test_sanity() -> None:
with open(filename, "rb") as test_file: with open(filename, "rb") as test_file:
font = BdfFontFile.BdfFontFile(test_file) font = BdfFontFile.BdfFontFile(test_file)
@ -14,7 +15,7 @@ def test_sanity():
assert len([_f for _f in font.glyph if _f]) == 190 assert len([_f for _f in font.glyph if _f]) == 190
def test_invalid_file(): def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
BdfFontFile.BdfFontFile(fp) BdfFontFile.BdfFontFile(fp)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
@ -7,7 +8,7 @@ from .helper import skip_unless_feature
class TestFontCrash: class TestFontCrash:
def _fuzz_font(self, font): def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
# from fuzzers.fuzz_font # from fuzzers.fuzz_font
font.getbbox("ABC") font.getbbox("ABC")
font.getmask("test text") font.getmask("test text")
@ -17,7 +18,7 @@ class TestFontCrash:
draw.text((10, 10), "Test Text", font=font, fill="#000") draw.text((10, 10), "Test Text", font=font, fill="#000")
@skip_unless_feature("freetype2") @skip_unless_feature("freetype2")
def test_segfault(self): def test_segfault(self) -> None:
with pytest.raises(OSError): with pytest.raises(OSError):
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font) self._fuzz_font(font)

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from .helper import PillowLeakTestCase, skip_unless_feature from .helper import PillowLeakTestCase, skip_unless_feature
@ -9,7 +10,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
iterations = 10 iterations = 10
mem_limit = 4096 # k mem_limit = 4096 # k
def _test_font(self, font): def _test_font(self, font: ImageFont.FreeTypeFont) -> None:
im = Image.new("RGB", (255, 255), "white") im = Image.new("RGB", (255, 255), "white")
draw = ImageDraw.ImageDraw(im) draw = ImageDraw.ImageDraw(im)
self._test_leak( self._test_leak(
@ -19,7 +20,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
) )
@skip_unless_feature("freetype2") @skip_unless_feature("freetype2")
def test_leak(self): def test_leak(self) -> None:
ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
self._test_font(ttype) self._test_font(ttype)
@ -29,6 +30,6 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
iterations = 100 iterations = 100
mem_limit = 1024 # k mem_limit = 1024 # k
def test_leak(self): def test_leak(self) -> None:
default_font = ImageFont.load_default() default_font = ImageFont.load_default()
self._test_font(default_font) self._test_font(default_font)

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
from pathlib import Path
import pytest import pytest
@ -19,7 +21,7 @@ message = "hello, world"
pytestmark = skip_unless_feature("zlib") pytestmark = skip_unless_feature("zlib")
def save_font(request, tmp_path): def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str:
with open(fontname, "rb") as test_file: with open(fontname, "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file) font = PcfFontFile.PcfFontFile(test_file)
assert isinstance(font, FontFile.FontFile) assert isinstance(font, FontFile.FontFile)
@ -28,7 +30,7 @@ def save_font(request, tmp_path):
tempname = str(tmp_path / "temp.pil") tempname = str(tmp_path / "temp.pil")
def delete_tempfile(): def delete_tempfile() -> None:
try: try:
os.remove(tempname[:-4] + ".pbm") os.remove(tempname[:-4] + ".pbm")
except OSError: except OSError:
@ -46,11 +48,11 @@ def save_font(request, tmp_path):
return tempname return tempname
def test_sanity(request, tmp_path): def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None:
save_font(request, tmp_path) save_font(request, tmp_path)
def test_less_than_256_characters(): def test_less_than_256_characters() -> None:
with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file) font = PcfFontFile.PcfFontFile(test_file)
assert isinstance(font, FontFile.FontFile) assert isinstance(font, FontFile.FontFile)
@ -58,13 +60,13 @@ def test_less_than_256_characters():
assert len([_f for _f in font.glyph if _f]) == 127 assert len([_f for _f in font.glyph if _f]) == 127
def test_invalid_file(): def test_invalid_file() -> None:
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
PcfFontFile.PcfFontFile(fp) PcfFontFile.PcfFontFile(fp)
def test_draw(request, tmp_path): def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None:
tempname = save_font(request, tmp_path) tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)
im = Image.new("L", (130, 30), "white") im = Image.new("L", (130, 30), "white")
@ -73,7 +75,7 @@ def test_draw(request, tmp_path):
assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0)
def test_textsize(request, tmp_path): def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
tempname = save_font(request, tmp_path) tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)
for i in range(255): for i in range(255):
@ -89,7 +91,9 @@ def test_textsize(request, tmp_path):
assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20)
def _test_high_characters(request, tmp_path, message): def _test_high_characters(
request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes
) -> None:
tempname = save_font(request, tmp_path) tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)
im = Image.new("L", (750, 30), "white") im = Image.new("L", (750, 30), "white")
@ -98,7 +102,7 @@ def _test_high_characters(request, tmp_path, message):
assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0)
def test_high_characters(request, tmp_path): def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None:
message = "".join(chr(i + 1) for i in range(140, 232)) message = "".join(chr(i + 1) for i in range(140, 232))
_test_high_characters(request, tmp_path, message) _test_high_characters(request, tmp_path, message)
# accept bytes instances. # accept bytes instances.

View File

@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os import os
from pathlib import Path
from typing import TypedDict
import pytest import pytest
@ -13,7 +16,14 @@ from .helper import (
fontname = "Tests/fonts/ter-x20b.pcf" fontname = "Tests/fonts/ter-x20b.pcf"
charsets = {
class Charset(TypedDict):
glyph_count: int
message: str
image1: str
charsets: dict[str, Charset] = {
"iso8859-1": { "iso8859-1": {
"glyph_count": 223, "glyph_count": 223,
"message": "hello, world", "message": "hello, world",
@ -35,7 +45,7 @@ charsets = {
pytestmark = skip_unless_feature("zlib") pytestmark = skip_unless_feature("zlib")
def save_font(request, tmp_path, encoding): def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str:
with open(fontname, "rb") as test_file: with open(fontname, "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file, encoding) font = PcfFontFile.PcfFontFile(test_file, encoding)
assert isinstance(font, FontFile.FontFile) assert isinstance(font, FontFile.FontFile)
@ -44,7 +54,7 @@ def save_font(request, tmp_path, encoding):
tempname = str(tmp_path / "temp.pil") tempname = str(tmp_path / "temp.pil")
def delete_tempfile(): def delete_tempfile() -> None:
try: try:
os.remove(tempname[:-4] + ".pbm") os.remove(tempname[:-4] + ".pbm")
except OSError: except OSError:
@ -63,12 +73,12 @@ def save_font(request, tmp_path, encoding):
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
def test_sanity(request, tmp_path, encoding): def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None:
save_font(request, tmp_path, encoding) save_font(request, tmp_path, encoding)
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
def test_draw(request, tmp_path, encoding): def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None:
tempname = save_font(request, tmp_path, encoding) tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)
im = Image.new("L", (150, 30), "white") im = Image.new("L", (150, 30), "white")
@ -79,7 +89,9 @@ def test_draw(request, tmp_path, encoding):
@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
def test_textsize(request, tmp_path, encoding): def test_textsize(
request: pytest.FixtureRequest, tmp_path: Path, encoding: str
) -> None:
tempname = save_font(request, tmp_path, encoding) tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)
for i in range(255): for i in range(255):

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from PIL import FontFile from PIL import FontFile

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