mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 01:46:18 +03:00
Merge branch 'main' into imagefont
This commit is contained in:
commit
cc094caaa4
|
@ -6,6 +6,7 @@ init:
|
||||||
# Uncomment previous line to get RDP access during the build.
|
# Uncomment previous line to get RDP access during the build.
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
EXECUTABLE: python.exe
|
EXECUTABLE: python.exe
|
||||||
TEST_OPTIONS:
|
TEST_OPTIONS:
|
||||||
DEPLOY: YES
|
DEPLOY: YES
|
||||||
|
|
1
.ci/requirements-mypy.txt
Normal file
1
.ci/requirements-mypy.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mypy==1.7.1
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
||||||
tidelift: "pypi/Pillow"
|
tidelift: "pypi/pillow"
|
||||||
|
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
|
@ -7,10 +7,12 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "src/PIL/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "src/PIL/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
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 }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
3
.github/workflows/test-mingw.yml
vendored
3
.github/workflows/test-mingw.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
17
.github/workflows/test-windows.yml
vendored
17
.github/workflows/test-windows.yml
vendored
|
@ -26,13 +26,16 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"]
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
@ -66,8 +69,16 @@ jobs:
|
||||||
- name: Print build system information
|
- name: Print build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
- name: Install Python dependencies
|
||||||
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
run: >
|
||||||
|
python3 -m pip install
|
||||||
|
coverage>=7.4.2
|
||||||
|
defusedxml
|
||||||
|
olefile
|
||||||
|
pyroma
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-timeout
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: install
|
id: install
|
||||||
|
|
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
|
@ -27,6 +27,7 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
jobs:
|
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
|
HARFBUZZ_VERSION=8.3.0
|
||||||
LIBPNG_VERSION=1.6.40
|
LIBPNG_VERSION=1.6.40
|
||||||
JPEGTURBO_VERSION=3.0.1
|
JPEGTURBO_VERSION=3.0.1
|
||||||
OPENJPEG_VERSION=2.5.0
|
OPENJPEG_VERSION=2.5.2
|
||||||
XZ_VERSION=5.4.5
|
XZ_VERSION=5.4.5
|
||||||
TIFF_VERSION=4.6.0
|
TIFF_VERSION=4.6.0
|
||||||
LCMS2_VERSION=2.16
|
LCMS2_VERSION=2.16
|
||||||
|
@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0
|
||||||
|
|
||||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||||
function build_openjpeg {
|
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 \
|
(cd $out_dir \
|
||||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||||
&& make install)
|
&& make install)
|
||||||
|
@ -93,6 +93,9 @@ function build {
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
build_openjpeg
|
build_openjpeg
|
||||||
|
if [ -f /usr/local/lib64/libopenjp2.so ]; then
|
||||||
|
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
|
||||||
|
fi
|
||||||
|
|
||||||
ORIGINAL_CFLAGS=$CFLAGS
|
ORIGINAL_CFLAGS=$CFLAGS
|
||||||
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
||||||
|
|
|
@ -5,6 +5,12 @@ Changelog (Pillow)
|
||||||
10.3.0 (unreleased)
|
10.3.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- 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
|
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
|
||||||
[nik012003, radarhere]
|
[nik012003, radarhere]
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ As of 2019, Pillow development is
|
||||||
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
||||||
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
||||||
alt="Tidelift"
|
alt="Tidelift"
|
||||||
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
|
src="https://tidelift.com/badges/package/pypi/pillow?style=flat"></a>
|
||||||
<a href="https://pypi.org/project/pillow/"><img
|
<a href="https://pypi.org/project/pillow/"><img
|
||||||
alt="Newest PyPI version"
|
alt="Newest PyPI version"
|
||||||
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
||||||
|
@ -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
|
<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"
|
alt="Join the chat at https://gitter.im/python-pillow/Pillow"
|
||||||
src="https://badges.gitter.im/python-pillow/Pillow.svg"></a>
|
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
|
<a href="https://fosstodon.org/@pillow"><img
|
||||||
alt="Follow on https://fosstodon.org/@pillow"
|
alt="Follow on https://fosstodon.org/@pillow"
|
||||||
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"
|
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
|
## 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
|
## Documentation
|
||||||
|
|
||||||
|
|
BIN
Tests/images/2422.flc
Normal file
BIN
Tests/images/2422.flc
Normal file
Binary file not shown.
|
@ -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):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old thing", version, "new thing")
|
_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""
|
expected = r""
|
||||||
with pytest.raises(RuntimeError, match=expected):
|
with pytest.raises(RuntimeError, match=expected):
|
||||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||||
|
@ -76,7 +76,7 @@ def test_replacement_and_action() -> None:
|
||||||
"Upgrade to new thing.",
|
"Upgrade to new thing.",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_action(action) -> None:
|
def test_action(action: str) -> None:
|
||||||
expected = (
|
expected = (
|
||||||
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||||
r"Upgrade to new thing\."
|
r"Upgrade to new thing\."
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ def test_version() -> None:
|
||||||
# Check the correctness of the convenience function
|
# Check the correctness of the convenience function
|
||||||
# and the format of version numbers
|
# and the format of version numbers
|
||||||
|
|
||||||
def test(name, function) -> None:
|
def test(name: str, function: Callable[[str], bool]) -> None:
|
||||||
version = features.version(name)
|
version = features.version(name)
|
||||||
if not features.check(name):
|
if not features.check(name):
|
||||||
assert version is None
|
assert version is None
|
||||||
|
@ -73,12 +74,12 @@ def test_libimagequant_version() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("feature", features.modules)
|
@pytest.mark.parametrize("feature", features.modules)
|
||||||
def test_check_modules(feature) -> None:
|
def test_check_modules(feature: str) -> None:
|
||||||
assert features.check_module(feature) in [True, False]
|
assert features.check_module(feature) in [True, False]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("feature", features.codecs)
|
@pytest.mark.parametrize("feature", features.codecs)
|
||||||
def test_check_codecs(feature) -> None:
|
def test_check_codecs(feature: str) -> None:
|
||||||
assert features.check_codec(feature) in [True, False]
|
assert features.check_codec(feature) in [True, False]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None:
|
||||||
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crashes(test_file) -> None:
|
def test_crashes(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -16,7 +16,7 @@ from .helper import (
|
||||||
|
|
||||||
|
|
||||||
def test_sanity(tmp_path: Path) -> None:
|
def test_sanity(tmp_path: Path) -> None:
|
||||||
def roundtrip(im) -> None:
|
def roundtrip(im: Image.Image) -> None:
|
||||||
outfile = str(tmp_path / "temp.bmp")
|
outfile = str(tmp_path / "temp.bmp")
|
||||||
|
|
||||||
im.save(outfile, "BMP")
|
im.save(outfile, "BMP")
|
||||||
|
@ -194,7 +194,7 @@ def test_rle4() -> None:
|
||||||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rle8_eof(file_name, length) -> None:
|
def test_rle8_eof(file_name: str, length: int) -> None:
|
||||||
with open(file_name, "rb") as fp:
|
with open(file_name, "rb") as fp:
|
||||||
data = fp.read(length)
|
data = fp.read(length)
|
||||||
with Image.open(io.BytesIO(data)) as im:
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
||||||
|
|
||||||
import pytest
|
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
|
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.
|
# save as...-> hopper.fli, default options.
|
||||||
static_test_file = "Tests/images/hopper.fli"
|
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"
|
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:
|
def test_sanity() -> None:
|
||||||
with Image.open(static_test_file) as im:
|
with Image.open(static_test_file) as im:
|
||||||
|
@ -32,6 +35,24 @@ def test_sanity() -> None:
|
||||||
assert im.is_animated
|
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")
|
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||||
def test_unclosed_file() -> None:
|
def test_unclosed_file() -> None:
|
||||||
def open() -> None:
|
def open() -> None:
|
||||||
|
|
|
@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
@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()
|
output = io.BytesIO()
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
|
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
|
||||||
|
|
|
@ -82,7 +82,7 @@ def test_eoferror() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
||||||
def test_roundtrip(mode, tmp_path: Path) -> None:
|
def test_roundtrip(mode: str, tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.im")
|
out = str(tmp_path / "temp.im")
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
|
@ -98,7 +98,7 @@ def test_i() -> None:
|
||||||
assert ret == 97
|
assert ret == 97
|
||||||
|
|
||||||
|
|
||||||
def test_dump(monkeypatch) -> None:
|
def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
c = b"abc"
|
c = b"abc"
|
||||||
# Temporarily redirect stdout
|
# Temporarily redirect stdout
|
||||||
|
|
|
@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
|
||||||
assert isinstance(im, MspImagePlugin.MspImageFile)
|
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:
|
with Image.open(source_path) as im:
|
||||||
assert_image_equal_tofile(im, target_path)
|
assert_image_equal_tofile(im, target_path)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
|
||||||
|
|
||||||
def _roundtrip(tmp_path: Path, im) -> None:
|
def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
|
||||||
f = str(tmp_path / "temp.pcx")
|
f = str(tmp_path / "temp.pcx")
|
||||||
im.save(f)
|
im.save(f)
|
||||||
with Image.open(f) as im2:
|
with Image.open(f) as im2:
|
||||||
|
@ -44,7 +44,7 @@ def test_invalid_file() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
||||||
def test_odd(tmp_path: Path, mode) -> None:
|
def test_odd(tmp_path: Path, mode: str) -> None:
|
||||||
# See issue #523, odd sized images should have a stride that's even.
|
# See issue #523, odd sized images should have a stride that's even.
|
||||||
# Not that ImageMagick or GIMP write PCX that way.
|
# Not that ImageMagick or GIMP write PCX that way.
|
||||||
# We were not handling properly.
|
# We were not handling properly.
|
||||||
|
@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None:
|
||||||
_roundtrip(tmp_path, im)
|
_roundtrip(tmp_path, im)
|
||||||
|
|
||||||
|
|
||||||
def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
|
def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
|
||||||
_last = ImageFile.MAXBLOCK
|
_last = ImageFile.MAXBLOCK
|
||||||
ImageFile.MAXBLOCK = size
|
ImageFile.MAXBLOCK = size
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -6,6 +6,7 @@ import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ from PIL import Image, PdfParser, features
|
||||||
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
|
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
|
||||||
|
@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
|
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
|
||||||
def test_save(tmp_path: Path, mode) -> None:
|
def test_save(tmp_path: Path, mode: str) -> None:
|
||||||
helper_save_as_pdf(tmp_path, mode)
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("jpg_2000")
|
@skip_unless_feature("jpg_2000")
|
||||||
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
||||||
def test_save_alpha(tmp_path: Path, mode) -> None:
|
def test_save_alpha(tmp_path: Path, mode: str) -> None:
|
||||||
helper_save_as_pdf(tmp_path, mode)
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None:
|
||||||
{"dpi": (75, 150), "resolution": 200},
|
{"dpi": (75, 150), "resolution": 200},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dpi(params, tmp_path: Path) -> None:
|
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
||||||
outfile = str(tmp_path / "temp.pdf")
|
outfile = str(tmp_path / "temp.pdf")
|
||||||
|
@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None:
|
||||||
assert os.path.getsize(outfile) > 0
|
assert os.path.getsize(outfile) > 0
|
||||||
|
|
||||||
# Test appending using a generator
|
# Test appending using a generator
|
||||||
def im_generator(ims):
|
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||||
yield from ims
|
yield from ims
|
||||||
|
|
||||||
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
||||||
|
@ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
|
||||||
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
|
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
|
||||||
|
|
||||||
|
|
||||||
def check_pdf_pages_consistency(pdf) -> None:
|
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
|
||||||
pages_info = pdf.read_indirect(pdf.pages_ref)
|
pages_info = pdf.read_indirect(pdf.pages_ref)
|
||||||
assert b"Parent" not in pages_info
|
assert b"Parent" not in pages_info
|
||||||
assert b"Kids" in pages_info
|
assert b"Kids" in pages_info
|
||||||
|
@ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None:
|
||||||
@pytest.mark.timeout(1)
|
@pytest.mark.timeout(1)
|
||||||
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
|
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
|
||||||
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
|
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
|
||||||
def test_redos(newline) -> None:
|
def test_redos(newline: bytes) -> None:
|
||||||
malicious = b" trailer<<>>" + newline * 3456
|
malicious = b" trailer<<>>" + newline * 3456
|
||||||
|
|
||||||
# This particular exception isn't relevant here.
|
# This particular exception isn't relevant here.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import warnings
|
||||||
import zlib
|
import zlib
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -23,6 +24,7 @@ from .helper import (
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ElementTree: ModuleType | None
|
||||||
try:
|
try:
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None:
|
||||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
("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 open(test_file, "rb") as f:
|
||||||
with pytest.raises(raises):
|
with pytest.raises(raises):
|
||||||
with Image.open(f):
|
with Image.open(f):
|
||||||
|
|
|
@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", _MODES)
|
@pytest.mark.parametrize("mode", _MODES)
|
||||||
def test_sanity(mode, tmp_path: Path) -> None:
|
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||||
def roundtrip(original_im) -> None:
|
def roundtrip(original_im: Image.Image) -> None:
|
||||||
out = str(tmp_path / "temp.tga")
|
out = str(tmp_path / "temp.tga")
|
||||||
|
|
||||||
original_im.save(out, rle=rle)
|
original_im.save(out, rle=rle)
|
||||||
|
|
|
@ -4,6 +4,8 @@ import os
|
||||||
import warnings
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -20,6 +22,7 @@ from .helper import (
|
||||||
is_win32,
|
is_win32,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ElementTree: ModuleType | None
|
||||||
try:
|
try:
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -156,7 +159,7 @@ class TestFileTiff:
|
||||||
"resolution_unit, dpi",
|
"resolution_unit, dpi",
|
||||||
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
||||||
)
|
)
|
||||||
def test_load_float_dpi(self, resolution_unit, dpi) -> None:
|
def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None:
|
||||||
with Image.open(
|
with Image.open(
|
||||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||||
) as im:
|
) as im:
|
||||||
|
@ -284,7 +287,7 @@ class TestFileTiff:
|
||||||
("Tests/images/multipage.tiff", 3),
|
("Tests/images/multipage.tiff", 3),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_n_frames(self, path, n_frames) -> None:
|
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
assert im.n_frames == n_frames
|
assert im.n_frames == n_frames
|
||||||
assert im.is_animated == (n_frames != 1)
|
assert im.is_animated == (n_frames != 1)
|
||||||
|
@ -402,7 +405,7 @@ class TestFileTiff:
|
||||||
assert len_before == len_after + 1
|
assert len_before == len_after + 1
|
||||||
|
|
||||||
@pytest.mark.parametrize("legacy_api", (False, True))
|
@pytest.mark.parametrize("legacy_api", (False, True))
|
||||||
def test_load_byte(self, legacy_api) -> None:
|
def test_load_byte(self, legacy_api: bool) -> None:
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
data = b"abc"
|
data = b"abc"
|
||||||
ret = ifd.load_byte(data, legacy_api)
|
ret = ifd.load_byte(data, legacy_api)
|
||||||
|
@ -431,7 +434,7 @@ class TestFileTiff:
|
||||||
assert 0x8825 in im.tag_v2
|
assert 0x8825 in im.tag_v2
|
||||||
|
|
||||||
def test_exif(self, tmp_path: Path) -> None:
|
def test_exif(self, tmp_path: Path) -> None:
|
||||||
def check_exif(exif) -> None:
|
def check_exif(exif: Image.Exif) -> None:
|
||||||
assert sorted(exif.keys()) == [
|
assert sorted(exif.keys()) == [
|
||||||
256,
|
256,
|
||||||
257,
|
257,
|
||||||
|
@ -511,7 +514,7 @@ class TestFileTiff:
|
||||||
assert im.getexif()[273] == (1408, 1907)
|
assert im.getexif()[273] == (1408, 1907)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||||
def test_photometric(self, mode, tmp_path: Path) -> None:
|
def test_photometric(self, mode: str, tmp_path: Path) -> None:
|
||||||
filename = str(tmp_path / "temp.tif")
|
filename = str(tmp_path / "temp.tif")
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(filename, tiffinfo={262: 0})
|
im.save(filename, tiffinfo={262: 0})
|
||||||
|
@ -660,7 +663,7 @@ class TestFileTiff:
|
||||||
assert_image_equal_tofile(reloaded, infile)
|
assert_image_equal_tofile(reloaded, infile)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
def test_palette(self, mode, tmp_path: Path) -> None:
|
def test_palette(self, mode: str, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
|
@ -689,7 +692,7 @@ class TestFileTiff:
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Test appending using a generator
|
# Test appending using a generator
|
||||||
def im_generator(ims):
|
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||||
yield from ims
|
yield from ims
|
||||||
|
|
||||||
mp = BytesIO()
|
mp = BytesIO()
|
||||||
|
@ -860,7 +863,7 @@ class TestFileTiff:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.timeout(2)
|
@pytest.mark.timeout(2)
|
||||||
def test_oom(self, test_file) -> None:
|
def test_oom(self, test_file: str) -> None:
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open(test_file):
|
with Image.open(test_file):
|
||||||
|
|
|
@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
|
@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()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
tag = TiffTags.TAGS_V2[271]
|
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)))
|
@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()
|
im = hopper()
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,18 @@ from __future__ import annotations
|
||||||
|
|
||||||
import colorsys
|
import colorsys
|
||||||
import itertools
|
import itertools
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import assert_image_similar, hopper
|
from .helper import assert_image_similar, hopper
|
||||||
|
|
||||||
|
|
||||||
def int_to_float(i):
|
def int_to_float(i: int) -> float:
|
||||||
return i / 255
|
return i / 255
|
||||||
|
|
||||||
|
|
||||||
def str_to_float(i):
|
def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]:
|
||||||
return ord(i) / 255
|
|
||||||
|
|
||||||
|
|
||||||
def tuple_to_ints(tp):
|
|
||||||
x, y, z = tp
|
x, y, z = tp
|
||||||
return int(x * 255.0), int(y * 255.0), int(z * 255.0)
|
return int(x * 255.0), int(y * 255.0), int(z * 255.0)
|
||||||
|
|
||||||
|
@ -25,7 +22,7 @@ def test_sanity() -> None:
|
||||||
Image.new("HSV", (100, 100))
|
Image.new("HSV", (100, 100))
|
||||||
|
|
||||||
|
|
||||||
def wedge():
|
def wedge() -> Image.Image:
|
||||||
w = Image._wedge()
|
w = Image._wedge()
|
||||||
w90 = w.rotate(90)
|
w90 = w.rotate(90)
|
||||||
|
|
||||||
|
@ -49,7 +46,11 @@ def wedge():
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
def to_xxx_colorsys(im, func, mode):
|
def to_xxx_colorsys(
|
||||||
|
im: Image.Image,
|
||||||
|
func: Callable[[float, float, float], tuple[float, float, float]],
|
||||||
|
mode: str,
|
||||||
|
) -> Image.Image:
|
||||||
# convert the hard way using the library colorsys routines.
|
# convert the hard way using the library colorsys routines.
|
||||||
|
|
||||||
(r, g, b) = im.split()
|
(r, g, b) = im.split()
|
||||||
|
@ -70,11 +71,11 @@ def to_xxx_colorsys(im, func, mode):
|
||||||
return hsv
|
return hsv
|
||||||
|
|
||||||
|
|
||||||
def to_hsv_colorsys(im):
|
def to_hsv_colorsys(im: Image.Image) -> Image.Image:
|
||||||
return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV")
|
return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV")
|
||||||
|
|
||||||
|
|
||||||
def to_rgb_colorsys(im):
|
def to_rgb_colorsys(im: Image.Image) -> Image.Image:
|
||||||
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
|
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -685,12 +685,15 @@ class TestImage:
|
||||||
_make_new(im, blank_p, ImagePalette.ImagePalette())
|
_make_new(im, blank_p, ImagePalette.ImagePalette())
|
||||||
_make_new(im, blank_pa, ImagePalette.ImagePalette())
|
_make_new(im, blank_pa, ImagePalette.ImagePalette())
|
||||||
|
|
||||||
def test_p_from_rgb_rgba(self) -> None:
|
@pytest.mark.parametrize(
|
||||||
for mode, color in [
|
"mode, color",
|
||||||
|
(
|
||||||
("RGB", "#DDEEFF"),
|
("RGB", "#DDEEFF"),
|
||||||
("RGB", (221, 238, 255)),
|
("RGB", (221, 238, 255)),
|
||||||
("RGBA", (221, 238, 255, 255)),
|
("RGBA", (221, 238, 255, 255)),
|
||||||
]:
|
),
|
||||||
|
)
|
||||||
|
def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None:
|
||||||
im = Image.new("P", (100, 100), color)
|
im = Image.new("P", (100, 100), color)
|
||||||
expected = Image.new(mode, (100, 100), color)
|
expected = Image.new(mode, (100, 100), color)
|
||||||
assert_image_equal(im.convert(mode), expected)
|
assert_image_equal(im.convert(mode), expected)
|
||||||
|
|
|
@ -8,7 +8,6 @@ from .helper import CachedProperty, assert_image_equal
|
||||||
|
|
||||||
|
|
||||||
class TestImagingPaste:
|
class TestImagingPaste:
|
||||||
masks = {}
|
|
||||||
size = 128
|
size = 128
|
||||||
|
|
||||||
def assert_9points_image(
|
def assert_9points_image(
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from PIL import Image, ImageChops
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
@ -387,7 +389,9 @@ def test_overlay() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_logical() -> None:
|
def test_logical() -> None:
|
||||||
def table(op, a, b):
|
def table(
|
||||||
|
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
|
||||||
|
) -> tuple[int, int, int, int]:
|
||||||
out = []
|
out = []
|
||||||
for x in (a, b):
|
for x in (a, b):
|
||||||
imx = Image.new("1", (1, 1), x)
|
imx = Image.new("1", (1, 1), x)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("flag", ("my string", -1))
|
@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 hopper() as im:
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
|
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
|
||||||
|
@ -335,19 +336,21 @@ def test_extended_information() -> None:
|
||||||
o = ImageCms.getOpenProfile(SRGB)
|
o = ImageCms.getOpenProfile(SRGB)
|
||||||
p = o.profile
|
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
|
# Helper function to reduce precision of tuples of floats
|
||||||
# recursively and then check equality.
|
# recursively and then check equality.
|
||||||
power = 10**digits
|
power = 10**digits
|
||||||
|
|
||||||
def truncate_tuple(tuple_or_float):
|
def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]:
|
||||||
return tuple(
|
return tuple(
|
||||||
(
|
(
|
||||||
truncate_tuple(val)
|
truncate_tuple(val)
|
||||||
if isinstance(val, tuple)
|
if isinstance(val, tuple)
|
||||||
else int(val * power) / power
|
else int(val * power) / power
|
||||||
)
|
)
|
||||||
for val in tuple_or_float
|
for val in tuple_value
|
||||||
)
|
)
|
||||||
|
|
||||||
assert truncate_tuple(tup1) == truncate_tuple(tup2)
|
assert truncate_tuple(tup1) == truncate_tuple(tup2)
|
||||||
|
@ -504,8 +507,10 @@ def test_profile_typesafety() -> None:
|
||||||
ImageCms.ImageCmsProfile(1).tobytes()
|
ImageCms.ImageCmsProfile(1).tobytes()
|
||||||
|
|
||||||
|
|
||||||
def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None:
|
def assert_aux_channel_preserved(
|
||||||
def create_test_image():
|
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.
|
# set up test image with something interesting in the tested aux channel.
|
||||||
# fmt: off
|
# fmt: off
|
||||||
nine_grid_deltas = [
|
nine_grid_deltas = [
|
||||||
|
@ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
@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))
|
im = Image.new(mode, (1, 1))
|
||||||
converted_im = im.convert("LAB")
|
converted_im = im.convert("LAB")
|
||||||
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import os.path
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageDraw2, features
|
from PIL import Image, ImageDraw, ImageDraw2, features
|
||||||
|
from PIL._typing import Coords
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -56,7 +57,7 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
def test_ellipse(bbox) -> None:
|
def test_ellipse(bbox: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -84,7 +85,7 @@ def test_ellipse_edge() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("points", POINTS)
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
def test_line(points) -> None:
|
def test_line(points: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -98,7 +99,7 @@ def test_line(points) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("points", POINTS)
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
def test_line_pen_as_brush(points) -> None:
|
def test_line_pen_as_brush(points: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("points", POINTS)
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
def test_polygon(points) -> None:
|
def test_polygon(points: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -129,7 +130,7 @@ def test_polygon(points) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
def test_rectangle(bbox) -> None:
|
def test_rectangle(bbox: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
|
|
@ -22,7 +22,7 @@ def test_crash() -> None:
|
||||||
ImageEnhance.Sharpness(im).enhance(0.5)
|
ImageEnhance.Sharpness(im).enhance(0.5)
|
||||||
|
|
||||||
|
|
||||||
def _half_transparent_image():
|
def _half_transparent_image() -> Image.Image:
|
||||||
# returns an image, half transparent, half solid
|
# returns an image, half transparent, half solid
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
|
|
||||||
|
@ -34,7 +34,9 @@ def _half_transparent_image():
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
def _check_alpha(im, original, op, amount) -> None:
|
def _check_alpha(
|
||||||
|
im: Image.Image, original: Image.Image, op: str, amount: float
|
||||||
|
) -> None:
|
||||||
assert im.getbands() == original.getbands()
|
assert im.getbands() == original.getbands()
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im.getchannel("A"),
|
im.getchannel("A"),
|
||||||
|
@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
|
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
|
||||||
def test_alpha(op) -> None:
|
def test_alpha(op: str) -> None:
|
||||||
# Issue https://github.com/python-pillow/Pillow/issues/899
|
# Issue https://github.com/python-pillow/Pillow/issues/899
|
||||||
# Is alpha preserved through image enhancement?
|
# Is alpha preserved through image enhancement?
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
|
||||||
|
|
||||||
class TestImageFile:
|
class TestImageFile:
|
||||||
def test_parser(self) -> None:
|
def test_parser(self) -> None:
|
||||||
def roundtrip(format):
|
def roundtrip(format: str) -> tuple[Image.Image, Image.Image]:
|
||||||
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
|
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
|
||||||
if format in ("MSP", "XBM"):
|
if format in ("MSP", "XBM"):
|
||||||
im = im.convert("1")
|
im = im.convert("1")
|
||||||
|
|
|
@ -7,11 +7,13 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, BinaryIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont, features
|
from PIL import Image, ImageDraw, ImageFont, features
|
||||||
|
from PIL._typing import StrOrBytesPath
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -42,16 +44,16 @@ def test_sanity() -> None:
|
||||||
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
|
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
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def font(layout_engine):
|
def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
|
||||||
return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
|
return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_font_properties(font) -> None:
|
def test_font_properties(font: ImageFont.FreeTypeFont) -> None:
|
||||||
assert font.path == FONT_PATH
|
assert font.path == FONT_PATH
|
||||||
assert font.size == FONT_SIZE
|
assert font.size == FONT_SIZE
|
||||||
|
|
||||||
|
@ -67,7 +69,9 @@ def test_font_properties(font) -> None:
|
||||||
assert font_copy.path == second_font_path
|
assert font_copy.path == second_font_path
|
||||||
|
|
||||||
|
|
||||||
def _render(font, layout_engine):
|
def _render(
|
||||||
|
font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout
|
||||||
|
) -> Image.Image:
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
|
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
|
||||||
ttf.getbbox(txt)
|
ttf.getbbox(txt)
|
||||||
|
@ -80,12 +84,12 @@ def _render(font, layout_engine):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
|
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
|
||||||
def test_font_with_name(layout_engine, font) -> None:
|
def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None:
|
||||||
_render(font, layout_engine)
|
_render(font, layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_font_with_filelike(layout_engine) -> None:
|
def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None:
|
||||||
def _font_as_bytes():
|
def _font_as_bytes() -> BytesIO:
|
||||||
with open(FONT_PATH, "rb") as f:
|
with open(FONT_PATH, "rb") as f:
|
||||||
font_bytes = BytesIO(f.read())
|
font_bytes = BytesIO(f.read())
|
||||||
return font_bytes
|
return font_bytes
|
||||||
|
@ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine) -> None:
|
||||||
# _render(shared_bytes)
|
# _render(shared_bytes)
|
||||||
|
|
||||||
|
|
||||||
def test_font_with_open_file(layout_engine) -> None:
|
def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None:
|
||||||
with open(FONT_PATH, "rb") as f:
|
with open(FONT_PATH, "rb") as f:
|
||||||
_render(f, layout_engine)
|
_render(f, layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_render_equal(layout_engine) -> None:
|
def test_render_equal(layout_engine: ImageFont.Layout) -> None:
|
||||||
img_path = _render(FONT_PATH, layout_engine)
|
img_path = _render(FONT_PATH, layout_engine)
|
||||||
with open(FONT_PATH, "rb") as f:
|
with open(FONT_PATH, "rb") as f:
|
||||||
font_filelike = BytesIO(f.read())
|
font_filelike = BytesIO(f.read())
|
||||||
|
@ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None:
|
||||||
assert_image_equal(img_path, img_filelike)
|
assert_image_equal(img_path, img_filelike)
|
||||||
|
|
||||||
|
|
||||||
def test_non_ascii_path(tmp_path: Path, layout_engine) -> None:
|
def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
|
||||||
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
|
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
|
||||||
try:
|
try:
|
||||||
shutil.copy(FONT_PATH, tempfile)
|
shutil.copy(FONT_PATH, tempfile)
|
||||||
|
@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None:
|
||||||
ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
|
ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_transparent_background(font) -> None:
|
def test_transparent_background(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGBA", size=(300, 100))
|
im = Image.new(mode="RGBA", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -140,7 +144,7 @@ def test_transparent_background(font) -> None:
|
||||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
||||||
|
|
||||||
def test_I16(font) -> None:
|
def test_I16(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="I;16", size=(300, 100))
|
im = Image.new(mode="I;16", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -153,7 +157,7 @@ def test_I16(font) -> None:
|
||||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
||||||
|
|
||||||
def test_textbbox_equal(font) -> None:
|
def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_getlength(
|
def test_getlength(
|
||||||
text, mode, fontname, size, layout_engine, length_basic, length_raqm
|
text: str,
|
||||||
|
mode: str,
|
||||||
|
fontname: str,
|
||||||
|
size: int,
|
||||||
|
layout_engine: ImageFont.Layout,
|
||||||
|
length_basic: int,
|
||||||
|
length_raqm: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
|
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
@ -207,7 +217,7 @@ def test_float_size() -> None:
|
||||||
assert lengths[0] != lengths[1] != lengths[2]
|
assert lengths[0] != lengths[1] != lengths[2]
|
||||||
|
|
||||||
|
|
||||||
def test_render_multiline(font) -> None:
|
def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
line_spacing = font.getbbox("A")[3] + 4
|
line_spacing = font.getbbox("A")[3] + 4
|
||||||
|
@ -223,7 +233,7 @@ def test_render_multiline(font) -> None:
|
||||||
assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
|
assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
|
||||||
|
|
||||||
|
|
||||||
def test_render_multiline_text(font) -> None:
|
def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# Test that text() correctly connects to multiline_text()
|
# Test that text() correctly connects to multiline_text()
|
||||||
# and that align defaults to left
|
# and that align defaults to left
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
|
@ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
|
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
|
||||||
)
|
)
|
||||||
def test_render_multiline_text_align(font, align, ext) -> None:
|
def test_render_multiline_text_align(
|
||||||
|
font: ImageFont.FreeTypeFont, align: str, ext: str
|
||||||
|
) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
|
draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
|
||||||
|
@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None:
|
||||||
assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
|
assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_align(font) -> None:
|
def test_unknown_align(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -260,14 +272,14 @@ def test_unknown_align(font) -> None:
|
||||||
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
|
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
|
||||||
|
|
||||||
|
|
||||||
def test_draw_align(font) -> None:
|
def test_draw_align(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new("RGB", (300, 100), "white")
|
im = Image.new("RGB", (300, 100), "white")
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
line = "some text"
|
line = "some text"
|
||||||
draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
|
draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
|
||||||
|
|
||||||
|
|
||||||
def test_multiline_bbox(font) -> None:
|
def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None:
|
||||||
draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
|
draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
|
||||||
|
|
||||||
|
|
||||||
def test_multiline_width(font) -> None:
|
def test_multiline_width(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -295,7 +307,7 @@ def test_multiline_width(font) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_multiline_spacing(font) -> None:
|
def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
|
draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
|
||||||
|
@ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
||||||
)
|
)
|
||||||
def test_rotated_transposed_font(font, orientation) -> None:
|
def test_rotated_transposed_font(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
img_gray = Image.new("L", (100, 100))
|
img_gray = Image.new("L", (100, 100))
|
||||||
draw = ImageDraw.Draw(img_gray)
|
draw = ImageDraw.Draw(img_gray)
|
||||||
word = "testing"
|
word = "testing"
|
||||||
|
@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None:
|
||||||
Image.Transpose.FLIP_TOP_BOTTOM,
|
Image.Transpose.FLIP_TOP_BOTTOM,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_unrotated_transposed_font(font, orientation) -> None:
|
def test_unrotated_transposed_font(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
img_gray = Image.new("L", (100, 100))
|
img_gray = Image.new("L", (100, 100))
|
||||||
draw = ImageDraw.Draw(img_gray)
|
draw = ImageDraw.Draw(img_gray)
|
||||||
word = "testing"
|
word = "testing"
|
||||||
|
@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
||||||
)
|
)
|
||||||
def test_rotated_transposed_font_get_mask(font, orientation) -> None:
|
def test_rotated_transposed_font_get_mask(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
text = "mask this"
|
text = "mask this"
|
||||||
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
||||||
|
@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None:
|
||||||
Image.Transpose.FLIP_TOP_BOTTOM,
|
Image.Transpose.FLIP_TOP_BOTTOM,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_unrotated_transposed_font_get_mask(font, orientation) -> None:
|
def test_unrotated_transposed_font_get_mask(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
text = "mask this"
|
text = "mask this"
|
||||||
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
||||||
|
@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None:
|
||||||
assert mask.size == (108, 13)
|
assert mask.size == (108, 13)
|
||||||
|
|
||||||
|
|
||||||
def test_free_type_font_get_name(font) -> None:
|
def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None:
|
||||||
assert ("FreeMono", "Regular") == font.getname()
|
assert ("FreeMono", "Regular") == font.getname()
|
||||||
|
|
||||||
|
|
||||||
def test_free_type_font_get_metrics(font) -> None:
|
def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None:
|
||||||
ascent, descent = font.getmetrics()
|
ascent, descent = font.getmetrics()
|
||||||
|
|
||||||
assert isinstance(ascent, int)
|
assert isinstance(ascent, int)
|
||||||
|
@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None:
|
||||||
assert (ascent, descent) == (16, 4)
|
assert (ascent, descent) == (16, 4)
|
||||||
|
|
||||||
|
|
||||||
def test_free_type_font_get_mask(font) -> None:
|
def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
text = "mask this"
|
text = "mask this"
|
||||||
|
|
||||||
|
@ -473,16 +493,16 @@ def test_default_font() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
||||||
def test_getbbox(font, mode) -> None:
|
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
|
||||||
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
||||||
|
|
||||||
|
|
||||||
def test_getbbox_empty(font) -> None:
|
def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# issue #2614, should not crash.
|
# issue #2614, should not crash.
|
||||||
assert (0, 0, 0, 0) == font.getbbox("")
|
assert (0, 0, 0, 0) == font.getbbox("")
|
||||||
|
|
||||||
|
|
||||||
def test_render_empty(font) -> None:
|
def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# issue 2666
|
# issue 2666
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
target = im.copy()
|
target = im.copy()
|
||||||
|
@ -492,7 +512,7 @@ def test_render_empty(font) -> None:
|
||||||
assert_image_equal(im, target)
|
assert_image_equal(im, target)
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_extended(layout_engine) -> None:
|
def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
|
||||||
# issue #3777
|
# issue #3777
|
||||||
text = "A\u278A\U0001F12B"
|
text = "A\u278A\U0001F12B"
|
||||||
target = "Tests/images/unicode_extended.png"
|
target = "Tests/images/unicode_extended.png"
|
||||||
|
@ -515,21 +535,23 @@ def test_unicode_extended(layout_engine) -> None:
|
||||||
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
|
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
|
||||||
)
|
)
|
||||||
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
||||||
def test_find_font(monkeypatch, platform, font_directory) -> None:
|
def test_find_font(
|
||||||
def _test_fake_loading_font(path_to_fake, fontname) -> None:
|
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
|
# Make a copy of FreeTypeFont so we can patch the original
|
||||||
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
|
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
|
||||||
with monkeypatch.context() as m:
|
with monkeypatch.context() as m:
|
||||||
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
|
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:
|
if filepath == path_to_fake:
|
||||||
return ImageFont._FreeTypeFont(
|
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)
|
m.setattr(ImageFont, "FreeTypeFont", loadable_font)
|
||||||
font = ImageFont.truetype(fontname)
|
font = ImageFont.truetype(fontname)
|
||||||
|
@ -543,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
|
||||||
if platform == "linux":
|
if platform == "linux":
|
||||||
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
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:
|
if path == font_directory:
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
|
@ -567,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
|
||||||
_test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
|
_test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
|
||||||
|
|
||||||
|
|
||||||
def test_imagefont_getters(font) -> None:
|
def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None:
|
||||||
assert font.getmetrics() == (16, 4)
|
assert font.getmetrics() == (16, 4)
|
||||||
assert font.font.ascent == 16
|
assert font.font.ascent == 16
|
||||||
assert font.font.descent == 4
|
assert font.font.descent == 4
|
||||||
|
@ -588,7 +610,7 @@ def test_imagefont_getters(font) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("stroke_width", (0, 2))
|
@pytest.mark.parametrize("stroke_width", (0, 2))
|
||||||
def test_getsize_stroke(font, stroke_width) -> None:
|
def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None:
|
||||||
assert font.getbbox("A", stroke_width=stroke_width) == (
|
assert font.getbbox("A", stroke_width=stroke_width) == (
|
||||||
0 - stroke_width,
|
0 - stroke_width,
|
||||||
4 - stroke_width,
|
4 - stroke_width,
|
||||||
|
@ -607,7 +629,7 @@ def test_complex_font_settings() -> None:
|
||||||
t.getmask("абвг", language="sr")
|
t.getmask("абвг", language="sr")
|
||||||
|
|
||||||
|
|
||||||
def test_variation_get(font) -> None:
|
def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
|
||||||
freetype = parse_version(features.version_module("freetype2"))
|
freetype = parse_version(features.version_module("freetype2"))
|
||||||
if freetype < parse_version("2.9.1"):
|
if freetype < parse_version("2.9.1"):
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
@ -662,7 +684,7 @@ def test_variation_get(font) -> None:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _check_text(font, path, epsilon):
|
def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None:
|
||||||
im = Image.new("RGB", (100, 75), "white")
|
im = Image.new("RGB", (100, 75), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.text((10, 10), "Text", font=font, fill="black")
|
d.text((10, 10), "Text", font=font, fill="black")
|
||||||
|
@ -677,7 +699,7 @@ def _check_text(font, path, epsilon):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_variation_set_by_name(font) -> None:
|
def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
|
||||||
freetype = parse_version(features.version_module("freetype2"))
|
freetype = parse_version(features.version_module("freetype2"))
|
||||||
if freetype < parse_version("2.9.1"):
|
if freetype < parse_version("2.9.1"):
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
@ -702,7 +724,7 @@ def test_variation_set_by_name(font) -> None:
|
||||||
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
|
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
|
||||||
|
|
||||||
|
|
||||||
def test_variation_set_by_axes(font) -> None:
|
def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
|
||||||
freetype = parse_version(features.version_module("freetype2"))
|
freetype = parse_version(features.version_module("freetype2"))
|
||||||
if freetype < parse_version("2.9.1"):
|
if freetype < parse_version("2.9.1"):
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
@ -737,7 +759,9 @@ def test_variation_set_by_axes(font) -> None:
|
||||||
),
|
),
|
||||||
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
|
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
|
||||||
)
|
)
|
||||||
def test_anchor(layout_engine, anchor, left, top) -> None:
|
def test_anchor(
|
||||||
|
layout_engine: ImageFont.Layout, anchor: str, left: int, top: int
|
||||||
|
) -> None:
|
||||||
name, text = "quick", "Quick"
|
name, text = "quick", "Quick"
|
||||||
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
|
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
|
||||||
|
|
||||||
|
@ -782,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None:
|
||||||
("md", "center"),
|
("md", "center"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_anchor_multiline(layout_engine, anchor, align) -> None:
|
def test_anchor_multiline(
|
||||||
|
layout_engine: ImageFont.Layout, anchor: str, align: str
|
||||||
|
) -> None:
|
||||||
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
|
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
|
||||||
text = "a\nlong\ntext sample"
|
text = "a\nlong\ntext sample"
|
||||||
|
|
||||||
|
@ -800,7 +826,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None:
|
||||||
assert_image_similar_tofile(im, target, 4)
|
assert_image_similar_tofile(im, target, 4)
|
||||||
|
|
||||||
|
|
||||||
def test_anchor_invalid(font) -> None:
|
def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new("RGB", (100, 100), "white")
|
im = Image.new("RGB", (100, 100), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.font = font
|
d.font = font
|
||||||
|
@ -826,7 +852,7 @@ def test_anchor_invalid(font) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
|
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
|
||||||
def test_bitmap_font(layout_engine, bpp) -> None:
|
def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None:
|
||||||
text = "Bitmap Font"
|
text = "Bitmap Font"
|
||||||
layout_name = ["basic", "raqm"][layout_engine]
|
layout_name = ["basic", "raqm"][layout_engine]
|
||||||
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
|
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
|
||||||
|
@ -843,7 +869,7 @@ def test_bitmap_font(layout_engine, bpp) -> None:
|
||||||
assert_image_equal_tofile(im, target)
|
assert_image_equal_tofile(im, target)
|
||||||
|
|
||||||
|
|
||||||
def test_bitmap_font_stroke(layout_engine) -> None:
|
def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None:
|
||||||
text = "Bitmap Font"
|
text = "Bitmap Font"
|
||||||
layout_name = ["basic", "raqm"][layout_engine]
|
layout_name = ["basic", "raqm"][layout_engine]
|
||||||
target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
|
target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
|
||||||
|
@ -861,7 +887,7 @@ def test_bitmap_font_stroke(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("embedded_color", (False, True))
|
@pytest.mark.parametrize("embedded_color", (False, True))
|
||||||
def test_bitmap_blend(layout_engine, embedded_color) -> None:
|
def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
"Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
)
|
)
|
||||||
|
@ -873,7 +899,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
|
assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
|
||||||
|
|
||||||
|
|
||||||
def test_standard_embedded_color(layout_engine) -> None:
|
def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None:
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
||||||
ttf.getbbox(txt)
|
ttf.getbbox(txt)
|
||||||
|
@ -886,7 +912,7 @@ def test_standard_embedded_color(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
|
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
|
||||||
def test_float_coord(layout_engine, fontmode):
|
def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None:
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
@ -908,7 +934,7 @@ def test_float_coord(layout_engine, fontmode):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_cbdt(layout_engine) -> None:
|
def test_cbdt(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
|
@ -925,7 +951,7 @@ def test_cbdt(layout_engine) -> None:
|
||||||
pytest.skip("freetype compiled without libpng or CBDT support")
|
pytest.skip("freetype compiled without libpng or CBDT support")
|
||||||
|
|
||||||
|
|
||||||
def test_cbdt_mask(layout_engine) -> None:
|
def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
|
@ -942,7 +968,7 @@ def test_cbdt_mask(layout_engine) -> None:
|
||||||
pytest.skip("freetype compiled without libpng or CBDT support")
|
pytest.skip("freetype compiled without libpng or CBDT support")
|
||||||
|
|
||||||
|
|
||||||
def test_sbix(layout_engine) -> None:
|
def test_sbix(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
||||||
|
@ -959,7 +985,7 @@ def test_sbix(layout_engine) -> None:
|
||||||
pytest.skip("freetype compiled without libpng or SBIX support")
|
pytest.skip("freetype compiled without libpng or SBIX support")
|
||||||
|
|
||||||
|
|
||||||
def test_sbix_mask(layout_engine) -> None:
|
def test_sbix_mask(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
||||||
|
@ -977,7 +1003,7 @@ def test_sbix_mask(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature_version("freetype2", "2.10.0")
|
@skip_unless_feature_version("freetype2", "2.10.0")
|
||||||
def test_colr(layout_engine) -> None:
|
def test_colr(layout_engine: ImageFont.Layout) -> None:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
||||||
size=64,
|
size=64,
|
||||||
|
@ -993,7 +1019,7 @@ def test_colr(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature_version("freetype2", "2.10.0")
|
@skip_unless_feature_version("freetype2", "2.10.0")
|
||||||
def test_colr_mask(layout_engine) -> None:
|
def test_colr_mask(layout_engine: ImageFont.Layout) -> None:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
||||||
size=64,
|
size=64,
|
||||||
|
@ -1008,7 +1034,7 @@ def test_colr_mask(layout_engine) -> None:
|
||||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
|
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
|
||||||
|
|
||||||
|
|
||||||
def test_woff2(layout_engine) -> None:
|
def test_woff2(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/OpenSans.woff2",
|
"Tests/fonts/OpenSans.woff2",
|
||||||
|
@ -1042,7 +1068,7 @@ def test_render_mono_size() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
|
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
|
||||||
|
|
||||||
|
|
||||||
def test_too_many_characters(font) -> None:
|
def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
font.getlength("A" * 1_000_001)
|
font.getlength("A" * 1_000_001)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -1070,14 +1096,14 @@ def test_too_many_characters(font) -> None:
|
||||||
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
|
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_oom(test_file) -> None:
|
def test_oom(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
font = ImageFont.truetype(BytesIO(f.read()))
|
font = ImageFont.truetype(BytesIO(f.read()))
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
font.getmask("Test Text")
|
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)
|
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
|
||||||
with pytest.warns(UserWarning) as record:
|
with pytest.warns(UserWarning) as record:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
|
@ -1091,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("size", [-1, 0])
|
@pytest.mark.parametrize("size", [-1, 0])
|
||||||
def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None:
|
def test_invalid_truetype_sizes_raise_valueerror(
|
||||||
|
layout_engine: ImageFont.Layout, size: int
|
||||||
|
) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
|
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
|
||||||
|
|
|
@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
||||||
def test_grabclipboard_file(self) -> None:
|
def test_grabclipboard_file(self) -> None:
|
||||||
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
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.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
|
@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
||||||
def test_grabclipboard_png(self) -> None:
|
def test_grabclipboard_png(self) -> None:
|
||||||
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
||||||
|
assert p.stdin is not None
|
||||||
p.stdin.write(
|
p.stdin.write(
|
||||||
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
|
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
|
||||||
$ms = new-object System.IO.MemoryStream(, $bytes)
|
$ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
|
@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
reason="Linux with wl-clipboard only",
|
reason="Linux with wl-clipboard only",
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
|
@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
|
image_path = "Tests/images/hopper." + ext
|
||||||
with open(image_path, "rb") as fp:
|
with open(image_path, "rb") as fp:
|
||||||
subprocess.call(["wl-copy"], stdin=fp)
|
subprocess.call(["wl-copy"], stdin=fp)
|
||||||
|
@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
reason="Linux with wl-clipboard only",
|
reason="Linux with wl-clipboard only",
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("arg", ("text", "--clear"))
|
@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])
|
subprocess.call(["wl-copy", arg])
|
||||||
assert ImageGrab.grabclipboard() is None
|
assert ImageGrab.grabclipboard() is None
|
||||||
|
|
|
@ -14,7 +14,7 @@ from .helper import (
|
||||||
|
|
||||||
|
|
||||||
class Deformer:
|
class Deformer:
|
||||||
def getmesh(self, im):
|
def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]:
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))]
|
return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))]
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ def test_fit_same_ratio() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
|
@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
|
||||||
def test_contain(new_size) -> None:
|
def test_contain(new_size: tuple[int, int]) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
new_im = ImageOps.contain(im, new_size)
|
new_im = ImageOps.contain(im, new_size)
|
||||||
assert new_im.size == (256, 256)
|
assert new_im.size == (256, 256)
|
||||||
|
@ -132,7 +132,7 @@ def test_contain_round() -> None:
|
||||||
("hopper.png", (256, 256)), # square
|
("hopper.png", (256, 256)), # square
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_cover(image_name, expected_size) -> None:
|
def test_cover(image_name: str, expected_size: tuple[int, int]) -> None:
|
||||||
with Image.open("Tests/images/" + image_name) as im:
|
with Image.open("Tests/images/" + image_name) as im:
|
||||||
new_im = ImageOps.cover(im, (256, 256))
|
new_im = ImageOps.cover(im, (256, 256))
|
||||||
assert new_im.size == expected_size
|
assert new_im.size == expected_size
|
||||||
|
@ -168,7 +168,7 @@ def test_pad_round() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
def test_palette(mode) -> None:
|
def test_palette(mode: str) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
|
|
||||||
# Expand
|
# Expand
|
||||||
|
@ -210,7 +210,7 @@ def test_scale() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
|
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
|
||||||
def test_expand_palette(border) -> None:
|
def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
|
||||||
with Image.open("Tests/images/p_16.tga") as im:
|
with Image.open("Tests/images/p_16.tga") as im:
|
||||||
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
|
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
|
||||||
|
|
||||||
|
@ -366,7 +366,7 @@ def test_exif_transpose() -> None:
|
||||||
for ext in exts:
|
for ext in exts:
|
||||||
with Image.open("Tests/images/hopper" + ext) as base_im:
|
with Image.open("Tests/images/hopper" + ext) as base_im:
|
||||||
|
|
||||||
def check(orientation_im) -> None:
|
def check(orientation_im: Image.Image) -> None:
|
||||||
for im in [
|
for im in [
|
||||||
orientation_im,
|
orientation_im,
|
||||||
orientation_im.copy(),
|
orientation_im.copy(),
|
||||||
|
@ -445,7 +445,7 @@ def test_autocontrast_cutoff() -> None:
|
||||||
# Test the cutoff argument of autocontrast
|
# Test the cutoff argument of autocontrast
|
||||||
with Image.open("Tests/images/bw_gradient.png") as img:
|
with Image.open("Tests/images/bw_gradient.png") as img:
|
||||||
|
|
||||||
def autocontrast(cutoff):
|
def autocontrast(cutoff: int | tuple[int, int]):
|
||||||
return ImageOps.autocontrast(img, cutoff).histogram()
|
return ImageOps.autocontrast(img, cutoff).histogram()
|
||||||
|
|
||||||
assert autocontrast(10) == autocontrast((10, 10))
|
assert autocontrast(10) == autocontrast((10, 10))
|
||||||
|
@ -486,20 +486,20 @@ def test_autocontrast_mask_real_input() -> None:
|
||||||
assert result_nomask != result
|
assert result_nomask != result
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
ImageStat.Stat(result, mask=rect_mask).median,
|
ImageStat.Stat(result, mask=rect_mask).median,
|
||||||
[195, 202, 184],
|
(195, 202, 184),
|
||||||
threshold=2,
|
threshold=2,
|
||||||
msg="autocontrast with mask pixel incorrect",
|
msg="autocontrast with mask pixel incorrect",
|
||||||
)
|
)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
ImageStat.Stat(result_nomask).median,
|
ImageStat.Stat(result_nomask).median,
|
||||||
[119, 106, 79],
|
(119, 106, 79),
|
||||||
threshold=2,
|
threshold=2,
|
||||||
msg="autocontrast without mask pixel incorrect",
|
msg="autocontrast without mask pixel incorrect",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_autocontrast_preserve_tone() -> None:
|
def test_autocontrast_preserve_tone() -> None:
|
||||||
def autocontrast(mode, preserve_tone):
|
def autocontrast(mode: str, preserve_tone: bool) -> Image.Image:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
|
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
|
||||||
|
|
||||||
|
@ -533,7 +533,7 @@ def test_autocontrast_preserve_gradient() -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
|
"color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
|
||||||
)
|
)
|
||||||
def test_autocontrast_preserve_one_color(color) -> None:
|
def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None:
|
||||||
img = Image.new("RGB", (10, 10), color)
|
img = Image.new("RGB", (10, 10), color)
|
||||||
|
|
||||||
# single color images shouldn't change
|
# single color images shouldn't change
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFilter
|
from PIL import Image, ImageFilter
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_images():
|
def test_images() -> Generator[dict[str, Image.Image], None, None]:
|
||||||
ims = {
|
ims = {
|
||||||
"im": Image.open("Tests/images/hopper.ppm"),
|
"im": Image.open("Tests/images/hopper.ppm"),
|
||||||
"snakes": Image.open("Tests/images/color_snakes.png"),
|
"snakes": Image.open("Tests/images/color_snakes.png"),
|
||||||
|
@ -18,7 +20,7 @@ def test_images():
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
|
||||||
def test_filter_api(test_images) -> None:
|
def test_filter_api(test_images: dict[str, Image.Image]) -> None:
|
||||||
im = test_images["im"]
|
im = test_images["im"]
|
||||||
|
|
||||||
test_filter = ImageFilter.GaussianBlur(2.0)
|
test_filter = ImageFilter.GaussianBlur(2.0)
|
||||||
|
@ -32,7 +34,7 @@ def test_filter_api(test_images) -> None:
|
||||||
assert i.size == (128, 128)
|
assert i.size == (128, 128)
|
||||||
|
|
||||||
|
|
||||||
def test_usm_formats(test_images) -> None:
|
def test_usm_formats(test_images: dict[str, Image.Image]) -> None:
|
||||||
im = test_images["im"]
|
im = test_images["im"]
|
||||||
|
|
||||||
usm = ImageFilter.UnsharpMask
|
usm = ImageFilter.UnsharpMask
|
||||||
|
@ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None:
|
||||||
im.convert("YCbCr").filter(usm)
|
im.convert("YCbCr").filter(usm)
|
||||||
|
|
||||||
|
|
||||||
def test_blur_formats(test_images) -> None:
|
def test_blur_formats(test_images: dict[str, Image.Image]) -> None:
|
||||||
im = test_images["im"]
|
im = test_images["im"]
|
||||||
|
|
||||||
blur = ImageFilter.GaussianBlur
|
blur = ImageFilter.GaussianBlur
|
||||||
|
@ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None:
|
||||||
im.convert("YCbCr").filter(blur)
|
im.convert("YCbCr").filter(blur)
|
||||||
|
|
||||||
|
|
||||||
def test_usm_accuracy(test_images) -> None:
|
def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None:
|
||||||
snakes = test_images["snakes"]
|
snakes = test_images["snakes"]
|
||||||
|
|
||||||
src = snakes.convert("RGB")
|
src = snakes.convert("RGB")
|
||||||
|
@ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None:
|
||||||
assert i.tobytes() == src.tobytes()
|
assert i.tobytes() == src.tobytes()
|
||||||
|
|
||||||
|
|
||||||
def test_blur_accuracy(test_images) -> None:
|
def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None:
|
||||||
snakes = test_images["snakes"]
|
snakes = test_images["snakes"]
|
||||||
|
|
||||||
i = snakes.filter(ImageFilter.GaussianBlur(0.4))
|
i = snakes.filter(ImageFilter.GaussianBlur(0.4))
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
import array
|
import array
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -57,7 +58,9 @@ def test_path() -> None:
|
||||||
ImagePath.Path((0, 1)),
|
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
|
# Arrange / Act
|
||||||
p = ImagePath.Path(coords)
|
p = ImagePath.Path(coords)
|
||||||
|
|
||||||
|
@ -75,7 +78,9 @@ def test_path_constructors(coords) -> None:
|
||||||
[[0.0, 1.0]],
|
[[0.0, 1.0]],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_invalid_path_constructors(coords) -> None:
|
def test_invalid_path_constructors(
|
||||||
|
coords: tuple[str, str] | Sequence[Sequence[int]]
|
||||||
|
) -> None:
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError) as e:
|
||||||
ImagePath.Path(coords)
|
ImagePath.Path(coords)
|
||||||
|
@ -93,7 +98,7 @@ def test_invalid_path_constructors(coords) -> None:
|
||||||
[0, 1, 2],
|
[0, 1, 2],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_path_odd_number_of_coordinates(coords) -> None:
|
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError) as e:
|
||||||
ImagePath.Path(coords)
|
ImagePath.Path(coords)
|
||||||
|
@ -111,7 +116,9 @@ def test_path_odd_number_of_coordinates(coords) -> None:
|
||||||
(1, (0.0, 0.0, 0.0, 0.0)),
|
(1, (0.0, 0.0, 0.0, 0.0)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_getbbox(coords, expected) -> None:
|
def test_getbbox(
|
||||||
|
coords: int | list[int], expected: tuple[float, float, float, float]
|
||||||
|
) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
p = ImagePath.Path(coords)
|
p = ImagePath.Path(coords)
|
||||||
|
|
||||||
|
@ -135,7 +142,7 @@ def test_getbbox_no_args() -> None:
|
||||||
(list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]),
|
(list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_map(coords, expected) -> None:
|
def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
p = ImagePath.Path(coords)
|
p = ImagePath.Path(coords)
|
||||||
|
|
||||||
|
@ -201,9 +208,9 @@ class Evil:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.corrupt = Image.core.path(0x4000000000000000)
|
self.corrupt = Image.core.path(0x4000000000000000)
|
||||||
|
|
||||||
def __getitem__(self, i):
|
def __getitem__(self, i: int) -> bytes:
|
||||||
x = self.corrupt[i]
|
x = self.corrupt[i]
|
||||||
return struct.pack("dd", x[0], x[1])
|
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)
|
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)
|
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 = ImageQt.rgb(r, g, b)
|
||||||
val = val % 2**24 # drop the alpha
|
val = val % 2**24 # drop the alpha
|
||||||
assert val >> 16 == r
|
assert val >> 16 == r
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageShow
|
from PIL import Image, ImageShow
|
||||||
|
@ -24,9 +26,9 @@ def test_register() -> None:
|
||||||
"order",
|
"order",
|
||||||
[-1, 0],
|
[-1, 0],
|
||||||
)
|
)
|
||||||
def test_viewer_show(order) -> None:
|
def test_viewer_show(order: int) -> None:
|
||||||
class TestViewer(ImageShow.Viewer):
|
class TestViewer(ImageShow.Viewer):
|
||||||
def show_image(self, image, **options) -> bool:
|
def show_image(self, image: Image.Image, **options: Any) -> bool:
|
||||||
self.methodCalled = True
|
self.methodCalled = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -48,7 +50,7 @@ def test_viewer_show(order) -> None:
|
||||||
reason="Only run on CIs; hangs on Windows CIs",
|
reason="Only run on CIs; hangs on Windows CIs",
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
|
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
|
||||||
def test_show(mode) -> None:
|
def test_show(mode: str) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
assert ImageShow.show(im)
|
assert ImageShow.show(im)
|
||||||
|
|
||||||
|
@ -73,7 +75,7 @@ def test_viewer() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("viewer", ImageShow._viewers)
|
@pytest.mark.parametrize("viewer", ImageShow._viewers)
|
||||||
def test_viewers(viewer) -> None:
|
def test_viewers(viewer: ImageShow.Viewer) -> None:
|
||||||
try:
|
try:
|
||||||
viewer.get_command("test.jpg")
|
viewer.get_command("test.jpg")
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
|
|
|
@ -70,7 +70,7 @@ if is_win32():
|
||||||
]
|
]
|
||||||
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
|
CreateDIBSection.restype = ctypes.wintypes.HBITMAP
|
||||||
|
|
||||||
def serialize_dib(bi, pixels):
|
def serialize_dib(bi, pixels) -> bytearray:
|
||||||
bf = BITMAPFILEHEADER()
|
bf = BITMAPFILEHEADER()
|
||||||
bf.bfType = 0x4D42
|
bf.bfType = 0x4D42
|
||||||
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
|
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
|
||||||
|
|
|
@ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10)
|
||||||
|
|
||||||
|
|
||||||
def test_numpy_to_image() -> None:
|
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 bands == 1:
|
||||||
if boolean:
|
if boolean:
|
||||||
data = [0, 255] * 50
|
data = [0, 255] * 50
|
||||||
|
@ -99,7 +99,7 @@ def test_1d_array() -> None:
|
||||||
assert_image(Image.fromarray(a), "L", (1, 5))
|
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
|
assert len(np.shape) >= 2
|
||||||
np_size = np.shape[1], np.shape[0]
|
np_size = np.shape[1], np.shape[0]
|
||||||
assert img.size == np_size
|
assert img.size == np_size
|
||||||
|
@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None:
|
||||||
("HSV", numpy.uint8),
|
("HSV", numpy.uint8),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_to_array(mode, dtype) -> None:
|
def test_to_array(mode: str, dtype) -> None:
|
||||||
img = hopper(mode)
|
img = hopper(mode)
|
||||||
|
|
||||||
# Resize to non-square
|
# Resize to non-square
|
||||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import ImageQt
|
from PIL import Image, ImageQt
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
|
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ if ImageQt.qt_is_installed:
|
||||||
lbl.setPixmap(pixmap1.copy())
|
lbl.setPixmap(pixmap1.copy())
|
||||||
|
|
||||||
|
|
||||||
def roundtrip(expected) -> None:
|
def roundtrip(expected: Image.Image) -> None:
|
||||||
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
|
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
|
||||||
# Qt saves all pixmaps as rgb
|
# Qt saves all pixmaps as rgb
|
||||||
assert_image_similar(result, expected.convert("RGB"), 1)
|
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"))
|
@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)
|
src = hopper(mode)
|
||||||
data = ImageQt.toqimage(src)
|
data = ImageQt.toqimage(src)
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from PIL import _util
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")]
|
"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
|
# Act
|
||||||
it_is = _util.is_path(test_path)
|
it_is = _util.is_path(test_path)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install openjpeg
|
# 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
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -326,7 +326,7 @@ linkcheck_allowed_redirects = {
|
||||||
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
|
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
|
||||||
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
|
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
|
||||||
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
|
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
|
||||||
r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
|
r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*",
|
||||||
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
|
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
|
||||||
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
|
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
|
||||||
}
|
}
|
||||||
|
|
|
@ -536,3 +536,27 @@ PIL.OleFileIO
|
||||||
the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
|
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
|
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
|
||||||
PyPI (eg. ``python3 -m pip install olefile``).
|
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.
|
||||||
|
|
|
@ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
|
||||||
:target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow
|
:target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow
|
||||||
:alt: Zenodo
|
:alt: Zenodo
|
||||||
|
|
||||||
.. image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat
|
.. image:: https://tidelift.com/badges/package/pypi/pillow?style=flat
|
||||||
:target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge
|
:target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge
|
||||||
:alt: Tidelift
|
:alt: Tidelift
|
||||||
|
|
||||||
|
@ -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
|
: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
|
: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
|
.. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg
|
||||||
:target: https://fosstodon.org/@pillow
|
:target: https://fosstodon.org/@pillow
|
||||||
:alt: Follow on https://fosstodon.org/@pillow
|
:alt: Follow on https://fosstodon.org/@pillow
|
||||||
|
|
|
@ -9,15 +9,6 @@ Installation
|
||||||
});
|
});
|
||||||
</script>
|
</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
|
Python Support
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -186,7 +177,7 @@ Many of Pillow's features require external libraries:
|
||||||
* **openjpeg** provides JPEG 2000 functionality.
|
* **openjpeg** provides JPEG 2000 functionality.
|
||||||
|
|
||||||
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
|
* 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
|
* Pillow does **not** support the earlier **1.5** series which ships
|
||||||
with Debian Jessie.
|
with Debian Jessie.
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ only work on L and RGB images.
|
||||||
.. autofunction:: colorize
|
.. autofunction:: colorize
|
||||||
.. autofunction:: crop
|
.. autofunction:: crop
|
||||||
.. autofunction:: scale
|
.. autofunction:: scale
|
||||||
|
.. autoclass:: SupportsGetMesh
|
||||||
|
:show-inheritance:
|
||||||
.. autofunction:: deform
|
.. autofunction:: deform
|
||||||
.. autofunction:: equalize
|
.. autofunction:: equalize
|
||||||
.. autofunction:: expand
|
.. autofunction:: expand
|
||||||
|
|
|
@ -79,3 +79,9 @@ Portable FloatMap (PFM) images
|
||||||
|
|
||||||
Support has been added for reading and writing grayscale (Pf format)
|
Support has been added for reading and writing grayscale (Pf format)
|
||||||
Portable FloatMap (PFM) files containing ``F`` data.
|
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 :: Capture :: Screen Capture",
|
||||||
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
||||||
"Topic :: Multimedia :: Graphics :: Viewers",
|
"Topic :: Multimedia :: Graphics :: Viewers",
|
||||||
|
"Typing :: Typed",
|
||||||
]
|
]
|
||||||
dynamic = [
|
dynamic = [
|
||||||
"version",
|
"version",
|
||||||
|
@ -79,7 +80,6 @@ Homepage = "https://python-pillow.org"
|
||||||
Mastodon = "https://fosstodon.org/@pillow"
|
Mastodon = "https://fosstodon.org/@pillow"
|
||||||
"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
|
"Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html"
|
||||||
Source = "https://github.com/python-pillow/Pillow"
|
Source = "https://github.com/python-pillow/Pillow"
|
||||||
Twitter = "https://twitter.com/PythonPillow"
|
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
packages = ["PIL"]
|
packages = ["PIL"]
|
||||||
|
@ -140,7 +140,3 @@ follow_imports = "silent"
|
||||||
warn_redundant_casts = true
|
warn_redundant_casts = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
warn_unused_ignores = true
|
warn_unused_ignores = true
|
||||||
exclude = [
|
|
||||||
'^src/PIL/FpxImagePlugin.py$',
|
|
||||||
'^src/PIL/MicImagePlugin.py$',
|
|
||||||
]
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile):
|
||||||
if i16(s, 4) == 0xF100:
|
if i16(s, 4) == 0xF100:
|
||||||
# prefix chunk; ignore it
|
# prefix chunk; ignore it
|
||||||
self.__offset = self.__offset + i32(s)
|
self.__offset = self.__offset + i32(s)
|
||||||
|
self.fp.seek(self.__offset)
|
||||||
s = self.fp.read(16)
|
s = self.fp.read(16)
|
||||||
|
|
||||||
if i16(s, 4) == 0xF1FA:
|
if i16(s, 4) == 0xF1FA:
|
||||||
|
|
|
@ -1430,7 +1430,7 @@ class Image:
|
||||||
root = ElementTree.fromstring(xmp_tags)
|
root = ElementTree.fromstring(xmp_tags)
|
||||||
return {get_name(root.tag): get_value(root)}
|
return {get_name(root.tag): get_value(root)}
|
||||||
|
|
||||||
def getexif(self):
|
def getexif(self) -> Exif:
|
||||||
"""
|
"""
|
||||||
Gets EXIF data from the image.
|
Gets EXIF data from the image.
|
||||||
|
|
||||||
|
@ -1438,7 +1438,6 @@ class Image:
|
||||||
"""
|
"""
|
||||||
if self._exif is None:
|
if self._exif is None:
|
||||||
self._exif = Exif()
|
self._exif = Exif()
|
||||||
self._exif._loaded = False
|
|
||||||
elif self._exif._loaded:
|
elif self._exif._loaded:
|
||||||
return self._exif
|
return self._exif
|
||||||
self._exif._loaded = True
|
self._exif._loaded = True
|
||||||
|
@ -1525,7 +1524,7 @@ class Image:
|
||||||
self.load()
|
self.load()
|
||||||
return self.im.ptr
|
return self.im.ptr
|
||||||
|
|
||||||
def getpalette(self, rawmode="RGB"):
|
def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None:
|
||||||
"""
|
"""
|
||||||
Returns the image palette as a list.
|
Returns the image palette as a list.
|
||||||
|
|
||||||
|
@ -1615,7 +1614,7 @@ class Image:
|
||||||
x, y = self.im.getprojection()
|
x, y = self.im.getprojection()
|
||||||
return list(x), list(y)
|
return list(x), list(y)
|
||||||
|
|
||||||
def histogram(self, mask=None, extrema=None):
|
def histogram(self, mask=None, extrema=None) -> list[int]:
|
||||||
"""
|
"""
|
||||||
Returns a histogram for the image. The histogram is returned as a
|
Returns a histogram for the image. The histogram is returned as a
|
||||||
list of pixel counts, one for each pixel value in the source
|
list of pixel counts, one for each pixel value in the source
|
||||||
|
@ -1804,7 +1803,7 @@ class Image:
|
||||||
result = alpha_composite(background, overlay)
|
result = alpha_composite(background, overlay)
|
||||||
self.paste(result, box)
|
self.paste(result, box)
|
||||||
|
|
||||||
def point(self, lut, mode=None):
|
def point(self, lut, mode: str | None = None) -> Image:
|
||||||
"""
|
"""
|
||||||
Maps this image through a lookup table or function.
|
Maps this image through a lookup table or function.
|
||||||
|
|
||||||
|
@ -1928,7 +1927,7 @@ class Image:
|
||||||
|
|
||||||
self.im.putdata(data, scale, offset)
|
self.im.putdata(data, scale, offset)
|
||||||
|
|
||||||
def putpalette(self, data, rawmode="RGB"):
|
def putpalette(self, data, rawmode="RGB") -> None:
|
||||||
"""
|
"""
|
||||||
Attaches a palette to this image. The image must be a "P", "PA", "L"
|
Attaches a palette to this image. The image must be a "P", "PA", "L"
|
||||||
or "LA" image.
|
or "LA" image.
|
||||||
|
@ -2108,7 +2107,7 @@ class Image:
|
||||||
min(self.size[1], math.ceil(box[3] + support_y)),
|
min(self.size[1], math.ceil(box[3] + support_y)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def resize(self, size, resample=None, box=None, reducing_gap=None):
|
def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image:
|
||||||
"""
|
"""
|
||||||
Returns a resized copy of this image.
|
Returns a resized copy of this image.
|
||||||
|
|
||||||
|
@ -2200,10 +2199,11 @@ class Image:
|
||||||
if factor_x > 1 or factor_y > 1:
|
if factor_x > 1 or factor_y > 1:
|
||||||
reduce_box = self._get_safe_box(size, resample, box)
|
reduce_box = self._get_safe_box(size, resample, box)
|
||||||
factor = (factor_x, factor_y)
|
factor = (factor_x, factor_y)
|
||||||
if callable(self.reduce):
|
self = (
|
||||||
self = self.reduce(factor, box=reduce_box)
|
self.reduce(factor, box=reduce_box)
|
||||||
else:
|
if callable(self.reduce)
|
||||||
self = Image.reduce(self, factor, box=reduce_box)
|
else Image.reduce(self, factor, box=reduce_box)
|
||||||
|
)
|
||||||
box = (
|
box = (
|
||||||
(box[0] - reduce_box[0]) / factor_x,
|
(box[0] - reduce_box[0]) / factor_x,
|
||||||
(box[1] - reduce_box[1]) / factor_y,
|
(box[1] - reduce_box[1]) / factor_y,
|
||||||
|
@ -2818,7 +2818,7 @@ class Image:
|
||||||
|
|
||||||
self.im.transform2(box, image.im, method, data, resample, fill)
|
self.im.transform2(box, image.im, method, data, resample, fill)
|
||||||
|
|
||||||
def transpose(self, method):
|
def transpose(self, method: Transpose) -> Image:
|
||||||
"""
|
"""
|
||||||
Transpose image (flip or rotate in 90 degree steps)
|
Transpose image (flip or rotate in 90 degree steps)
|
||||||
|
|
||||||
|
@ -2870,6 +2870,8 @@ class ImagePointHandler:
|
||||||
(for use with :py:meth:`~PIL.Image.Image.point`)
|
(for use with :py:meth:`~PIL.Image.Image.point`)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def point(self, im: Image) -> Image:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -3690,6 +3692,7 @@ class Exif(_ExifBase):
|
||||||
|
|
||||||
endian = None
|
endian = None
|
||||||
bigtiff = False
|
bigtiff = False
|
||||||
|
_loaded = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._data = {}
|
self._data = {}
|
||||||
|
@ -3805,7 +3808,7 @@ class Exif(_ExifBase):
|
||||||
|
|
||||||
return merged_dict
|
return merged_dict
|
||||||
|
|
||||||
def tobytes(self, offset=8):
|
def tobytes(self, offset: int = 8) -> bytes:
|
||||||
from . import TiffImagePlugin
|
from . import TiffImagePlugin
|
||||||
|
|
||||||
head = self._get_head()
|
head = self._get_head()
|
||||||
|
@ -3960,7 +3963,7 @@ class Exif(_ExifBase):
|
||||||
del self._info[tag]
|
del self._info[tag]
|
||||||
self._data[tag] = value
|
self._data[tag] = value
|
||||||
|
|
||||||
def __delitem__(self, tag):
|
def __delitem__(self, tag: int) -> None:
|
||||||
if self._info is not None and tag in self._info:
|
if self._info is not None and tag in self._info:
|
||||||
del self._info[tag]
|
del self._info[tag]
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -124,7 +124,7 @@ def getrgb(color):
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def getcolor(color, mode):
|
def getcolor(color, mode: str) -> tuple[int, ...]:
|
||||||
"""
|
"""
|
||||||
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
|
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
|
||||||
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
|
``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
|
||||||
|
|
|
@ -21,6 +21,7 @@ from __future__ import annotations
|
||||||
import functools
|
import functools
|
||||||
import operator
|
import operator
|
||||||
import re
|
import re
|
||||||
|
from typing import Protocol, Sequence, cast
|
||||||
|
|
||||||
from . import ExifTags, Image, ImagePalette
|
from . import ExifTags, Image, ImagePalette
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette
|
||||||
# helpers
|
# helpers
|
||||||
|
|
||||||
|
|
||||||
def _border(border):
|
def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
|
||||||
if isinstance(border, tuple):
|
if isinstance(border, tuple):
|
||||||
if len(border) == 2:
|
if len(border) == 2:
|
||||||
left, top = right, bottom = border
|
left, top = right, bottom = border
|
||||||
|
@ -39,7 +40,7 @@ def _border(border):
|
||||||
return left, top, right, bottom
|
return left, top, right, bottom
|
||||||
|
|
||||||
|
|
||||||
def _color(color, mode):
|
def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
|
||||||
if isinstance(color, str):
|
if isinstance(color, str):
|
||||||
from . import ImageColor
|
from . import ImageColor
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ def _color(color, mode):
|
||||||
return color
|
return color
|
||||||
|
|
||||||
|
|
||||||
def _lut(image, lut):
|
def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
|
||||||
if image.mode == "P":
|
if image.mode == "P":
|
||||||
# FIXME: apply to lookup table, not image data
|
# FIXME: apply to lookup table, not image data
|
||||||
msg = "mode P support coming soon"
|
msg = "mode P support coming soon"
|
||||||
|
@ -65,7 +66,13 @@ def _lut(image, lut):
|
||||||
# actions
|
# actions
|
||||||
|
|
||||||
|
|
||||||
def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
|
def autocontrast(
|
||||||
|
image: Image.Image,
|
||||||
|
cutoff: float | tuple[float, float] = 0,
|
||||||
|
ignore: int | Sequence[int] | None = None,
|
||||||
|
mask: Image.Image | None = None,
|
||||||
|
preserve_tone: bool = False,
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Maximize (normalize) image contrast. This function calculates a
|
Maximize (normalize) image contrast. This function calculates a
|
||||||
histogram of the input image (or mask region), removes ``cutoff`` percent of the
|
histogram of the input image (or mask region), removes ``cutoff`` percent of the
|
||||||
|
@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
|
||||||
h = histogram[layer : layer + 256]
|
h = histogram[layer : layer + 256]
|
||||||
if ignore is not None:
|
if ignore is not None:
|
||||||
# get rid of outliers
|
# get rid of outliers
|
||||||
try:
|
if isinstance(ignore, int):
|
||||||
h[ignore] = 0
|
h[ignore] = 0
|
||||||
except TypeError:
|
else:
|
||||||
# assume sequence
|
|
||||||
for ix in ignore:
|
for ix in ignore:
|
||||||
h[ix] = 0
|
h[ix] = 0
|
||||||
if cutoff:
|
if cutoff:
|
||||||
|
@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
|
||||||
for ix in range(256):
|
for ix in range(256):
|
||||||
n = n + h[ix]
|
n = n + h[ix]
|
||||||
# remove cutoff% pixels from the low end
|
# remove cutoff% pixels from the low end
|
||||||
cut = n * cutoff[0] // 100
|
cut = int(n * cutoff[0] // 100)
|
||||||
for lo in range(256):
|
for lo in range(256):
|
||||||
if cut > h[lo]:
|
if cut > h[lo]:
|
||||||
cut = cut - h[lo]
|
cut = cut - h[lo]
|
||||||
|
@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
|
||||||
if cut <= 0:
|
if cut <= 0:
|
||||||
break
|
break
|
||||||
# remove cutoff% samples from the high end
|
# remove cutoff% samples from the high end
|
||||||
cut = n * cutoff[1] // 100
|
cut = int(n * cutoff[1] // 100)
|
||||||
for hi in range(255, -1, -1):
|
for hi in range(255, -1, -1):
|
||||||
if cut > h[hi]:
|
if cut > h[hi]:
|
||||||
cut = cut - h[hi]
|
cut = cut - h[hi]
|
||||||
|
@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
|
||||||
return _lut(image, lut)
|
return _lut(image, lut)
|
||||||
|
|
||||||
|
|
||||||
def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127):
|
def colorize(
|
||||||
|
image: Image.Image,
|
||||||
|
black: str | tuple[int, ...],
|
||||||
|
white: str | tuple[int, ...],
|
||||||
|
mid: str | int | tuple[int, ...] | None = None,
|
||||||
|
blackpoint: int = 0,
|
||||||
|
whitepoint: int = 255,
|
||||||
|
midpoint: int = 127,
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Colorize grayscale image.
|
Colorize grayscale image.
|
||||||
This function calculates a color wedge which maps all black pixels in
|
This function calculates a color wedge which maps all black pixels in
|
||||||
|
@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
|
||||||
assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
|
assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
|
||||||
|
|
||||||
# Define colors from arguments
|
# Define colors from arguments
|
||||||
black = _color(black, "RGB")
|
rgb_black = cast(Sequence[int], _color(black, "RGB"))
|
||||||
white = _color(white, "RGB")
|
rgb_white = cast(Sequence[int], _color(white, "RGB"))
|
||||||
if mid is not None:
|
rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
|
||||||
mid = _color(mid, "RGB")
|
|
||||||
|
|
||||||
# Empty lists for the mapping
|
# Empty lists for the mapping
|
||||||
red = []
|
red = []
|
||||||
|
@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
|
||||||
|
|
||||||
# Create the low-end values
|
# Create the low-end values
|
||||||
for i in range(0, blackpoint):
|
for i in range(0, blackpoint):
|
||||||
red.append(black[0])
|
red.append(rgb_black[0])
|
||||||
green.append(black[1])
|
green.append(rgb_black[1])
|
||||||
blue.append(black[2])
|
blue.append(rgb_black[2])
|
||||||
|
|
||||||
# Create the mapping (2-color)
|
# Create the mapping (2-color)
|
||||||
if mid is None:
|
if rgb_mid is None:
|
||||||
range_map = range(0, whitepoint - blackpoint)
|
range_map = range(0, whitepoint - blackpoint)
|
||||||
|
|
||||||
for i in range_map:
|
for i in range_map:
|
||||||
red.append(black[0] + i * (white[0] - black[0]) // len(range_map))
|
red.append(
|
||||||
green.append(black[1] + i * (white[1] - black[1]) // len(range_map))
|
rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
|
||||||
blue.append(black[2] + i * (white[2] - black[2]) // len(range_map))
|
)
|
||||||
|
green.append(
|
||||||
|
rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
|
||||||
|
)
|
||||||
|
blue.append(
|
||||||
|
rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
|
||||||
|
)
|
||||||
|
|
||||||
# Create the mapping (3-color)
|
# Create the mapping (3-color)
|
||||||
else:
|
else:
|
||||||
|
@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
|
||||||
range_map2 = range(0, whitepoint - midpoint)
|
range_map2 = range(0, whitepoint - midpoint)
|
||||||
|
|
||||||
for i in range_map1:
|
for i in range_map1:
|
||||||
red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1))
|
red.append(
|
||||||
green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1))
|
rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
|
||||||
blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1))
|
)
|
||||||
|
green.append(
|
||||||
|
rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
|
||||||
|
)
|
||||||
|
blue.append(
|
||||||
|
rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
|
||||||
|
)
|
||||||
for i in range_map2:
|
for i in range_map2:
|
||||||
red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2))
|
red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
|
||||||
green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2))
|
green.append(
|
||||||
blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2))
|
rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
|
||||||
|
)
|
||||||
|
blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
|
||||||
|
|
||||||
# Create the high-end values
|
# Create the high-end values
|
||||||
for i in range(0, 256 - whitepoint):
|
for i in range(0, 256 - whitepoint):
|
||||||
red.append(white[0])
|
red.append(rgb_white[0])
|
||||||
green.append(white[1])
|
green.append(rgb_white[1])
|
||||||
blue.append(white[2])
|
blue.append(rgb_white[2])
|
||||||
|
|
||||||
# Return converted image
|
# Return converted image
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
return _lut(image, red + green + blue)
|
return _lut(image, red + green + blue)
|
||||||
|
|
||||||
|
|
||||||
def contain(image, size, method=Image.Resampling.BICUBIC):
|
def contain(
|
||||||
|
image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Returns a resized version of the image, set to the maximum width and height
|
Returns a resized version of the image, set to the maximum width and height
|
||||||
within the requested size, while maintaining the original aspect ratio.
|
within the requested size, while maintaining the original aspect ratio.
|
||||||
|
@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
|
||||||
return image.resize(size, resample=method)
|
return image.resize(size, resample=method)
|
||||||
|
|
||||||
|
|
||||||
def cover(image, size, method=Image.Resampling.BICUBIC):
|
def cover(
|
||||||
|
image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Returns a resized version of the image, so that the requested size is
|
Returns a resized version of the image, so that the requested size is
|
||||||
covered, while maintaining the original aspect ratio.
|
covered, while maintaining the original aspect ratio.
|
||||||
|
@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC):
|
||||||
return image.resize(size, resample=method)
|
return image.resize(size, resample=method)
|
||||||
|
|
||||||
|
|
||||||
def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
|
def pad(
|
||||||
|
image: Image.Image,
|
||||||
|
size: tuple[int, int],
|
||||||
|
method: int = Image.Resampling.BICUBIC,
|
||||||
|
color: str | int | tuple[int, ...] | None = None,
|
||||||
|
centering: tuple[float, float] = (0.5, 0.5),
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Returns a resized and padded version of the image, expanded to fill the
|
Returns a resized and padded version of the image, expanded to fill the
|
||||||
requested aspect ratio and size.
|
requested aspect ratio and size.
|
||||||
|
@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def crop(image, border=0):
|
def crop(image: Image.Image, border: int = 0) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Remove border from image. The same amount of pixels are removed
|
Remove border from image. The same amount of pixels are removed
|
||||||
from all four sides. This function works on all image modes.
|
from all four sides. This function works on all image modes.
|
||||||
|
@ -349,7 +386,9 @@ def crop(image, border=0):
|
||||||
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
|
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
|
||||||
|
|
||||||
|
|
||||||
def scale(image, factor, resample=Image.Resampling.BICUBIC):
|
def scale(
|
||||||
|
image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Returns a rescaled image by a specific factor given in parameter.
|
Returns a rescaled image by a specific factor given in parameter.
|
||||||
A factor greater than 1 expands the image, between 0 and 1 contracts the
|
A factor greater than 1 expands the image, between 0 and 1 contracts the
|
||||||
|
@ -372,7 +411,27 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC):
|
||||||
return image.resize(size, resample)
|
return image.resize(size, resample)
|
||||||
|
|
||||||
|
|
||||||
def deform(image, deformer, resample=Image.Resampling.BILINEAR):
|
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[
|
||||||
|
tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
|
||||||
|
]: ...
|
||||||
|
|
||||||
|
|
||||||
|
def deform(
|
||||||
|
image: Image.Image,
|
||||||
|
deformer: SupportsGetMesh,
|
||||||
|
resample: int = Image.Resampling.BILINEAR,
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Deform the image.
|
Deform the image.
|
||||||
|
|
||||||
|
@ -388,7 +447,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def equalize(image, mask=None):
|
def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Equalize the image histogram. This function applies a non-linear
|
Equalize the image histogram. This function applies a non-linear
|
||||||
mapping to the input image, in order to create a uniform
|
mapping to the input image, in order to create a uniform
|
||||||
|
@ -419,7 +478,11 @@ def equalize(image, mask=None):
|
||||||
return _lut(image, lut)
|
return _lut(image, lut)
|
||||||
|
|
||||||
|
|
||||||
def expand(image, border=0, fill=0):
|
def expand(
|
||||||
|
image: Image.Image,
|
||||||
|
border: int | tuple[int, ...] = 0,
|
||||||
|
fill: str | int | tuple[int, ...] = 0,
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Add border to the image
|
Add border to the image
|
||||||
|
|
||||||
|
@ -445,7 +508,13 @@ def expand(image, border=0, fill=0):
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
|
def fit(
|
||||||
|
image: Image.Image,
|
||||||
|
size: tuple[int, int],
|
||||||
|
method: int = Image.Resampling.BICUBIC,
|
||||||
|
bleed: float = 0.0,
|
||||||
|
centering: tuple[float, float] = (0.5, 0.5),
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Returns a resized and cropped version of the image, cropped to the
|
Returns a resized and cropped version of the image, cropped to the
|
||||||
requested aspect ratio and size.
|
requested aspect ratio and size.
|
||||||
|
@ -479,13 +548,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
|
||||||
# kevin@cazabon.com
|
# kevin@cazabon.com
|
||||||
# https://www.cazabon.com
|
# https://www.cazabon.com
|
||||||
|
|
||||||
# ensure centering is mutable
|
centering_x, centering_y = centering
|
||||||
centering = list(centering)
|
|
||||||
|
|
||||||
if not 0.0 <= centering[0] <= 1.0:
|
if not 0.0 <= centering_x <= 1.0:
|
||||||
centering[0] = 0.5
|
centering_x = 0.5
|
||||||
if not 0.0 <= centering[1] <= 1.0:
|
if not 0.0 <= centering_y <= 1.0:
|
||||||
centering[1] = 0.5
|
centering_y = 0.5
|
||||||
|
|
||||||
if not 0.0 <= bleed < 0.5:
|
if not 0.0 <= bleed < 0.5:
|
||||||
bleed = 0.0
|
bleed = 0.0
|
||||||
|
@ -522,8 +590,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
|
||||||
crop_height = live_size[0] / output_ratio
|
crop_height = live_size[0] / output_ratio
|
||||||
|
|
||||||
# make the crop
|
# make the crop
|
||||||
crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0]
|
crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
|
||||||
crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1]
|
crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
|
||||||
|
|
||||||
crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
|
crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
|
||||||
|
|
||||||
|
@ -531,7 +599,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5,
|
||||||
return image.resize(size, method, box=crop)
|
return image.resize(size, method, box=crop)
|
||||||
|
|
||||||
|
|
||||||
def flip(image):
|
def flip(image: Image.Image) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Flip the image vertically (top to bottom).
|
Flip the image vertically (top to bottom).
|
||||||
|
|
||||||
|
@ -541,7 +609,7 @@ def flip(image):
|
||||||
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
||||||
|
|
||||||
|
|
||||||
def grayscale(image):
|
def grayscale(image: Image.Image) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Convert the image to grayscale.
|
Convert the image to grayscale.
|
||||||
|
|
||||||
|
@ -551,7 +619,7 @@ def grayscale(image):
|
||||||
return image.convert("L")
|
return image.convert("L")
|
||||||
|
|
||||||
|
|
||||||
def invert(image):
|
def invert(image: Image.Image) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Invert (negate) the image.
|
Invert (negate) the image.
|
||||||
|
|
||||||
|
@ -562,7 +630,7 @@ def invert(image):
|
||||||
return image.point(lut) if image.mode == "1" else _lut(image, lut)
|
return image.point(lut) if image.mode == "1" else _lut(image, lut)
|
||||||
|
|
||||||
|
|
||||||
def mirror(image):
|
def mirror(image: Image.Image) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Flip image horizontally (left to right).
|
Flip image horizontally (left to right).
|
||||||
|
|
||||||
|
@ -572,7 +640,7 @@ def mirror(image):
|
||||||
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||||
|
|
||||||
|
|
||||||
def posterize(image, bits):
|
def posterize(image: Image.Image, bits: int) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Reduce the number of bits for each color channel.
|
Reduce the number of bits for each color channel.
|
||||||
|
|
||||||
|
@ -585,7 +653,7 @@ def posterize(image, bits):
|
||||||
return _lut(image, lut)
|
return _lut(image, lut)
|
||||||
|
|
||||||
|
|
||||||
def solarize(image, threshold=128):
|
def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Invert all pixel values above a threshold.
|
Invert all pixel values above a threshold.
|
||||||
|
|
||||||
|
@ -602,7 +670,7 @@ def solarize(image, threshold=128):
|
||||||
return _lut(image, lut)
|
return _lut(image, lut)
|
||||||
|
|
||||||
|
|
||||||
def exif_transpose(image, *, in_place=False):
|
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
|
||||||
"""
|
"""
|
||||||
If an image has an EXIF Orientation tag, other than 1, transpose the image
|
If an image has an EXIF Orientation tag, other than 1, transpose the image
|
||||||
accordingly, and remove the orientation data.
|
accordingly, and remove the orientation data.
|
||||||
|
@ -616,7 +684,7 @@ def exif_transpose(image, *, in_place=False):
|
||||||
"""
|
"""
|
||||||
image.load()
|
image.load()
|
||||||
image_exif = image.getexif()
|
image_exif = image.getexif()
|
||||||
orientation = image_exif.get(ExifTags.Base.Orientation)
|
orientation = image_exif.get(ExifTags.Base.Orientation, 1)
|
||||||
method = {
|
method = {
|
||||||
2: Image.Transpose.FLIP_LEFT_RIGHT,
|
2: Image.Transpose.FLIP_LEFT_RIGHT,
|
||||||
3: Image.Transpose.ROTATE_180,
|
3: Image.Transpose.ROTATE_180,
|
||||||
|
@ -653,3 +721,4 @@ def exif_transpose(image, *, in_place=False):
|
||||||
return transposed_image
|
return transposed_image
|
||||||
elif not in_place:
|
elif not in_place:
|
||||||
return image.copy()
|
return image.copy()
|
||||||
|
return None
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import array
|
import array
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
||||||
|
|
||||||
|
@ -34,11 +35,11 @@ class ImagePalette:
|
||||||
Defaults to an empty palette.
|
Defaults to an empty palette.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, mode="RGB", palette=None):
|
def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None:
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.rawmode = None # if set, palette contains raw data
|
self.rawmode = None # if set, palette contains raw data
|
||||||
self.palette = palette or bytearray()
|
self.palette = palette or bytearray()
|
||||||
self.dirty = None
|
self.dirty: int | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def palette(self):
|
def palette(self):
|
||||||
|
@ -127,7 +128,7 @@ class ImagePalette:
|
||||||
raise ValueError(msg) from e
|
raise ValueError(msg) from e
|
||||||
return index
|
return index
|
||||||
|
|
||||||
def getcolor(self, color, image=None):
|
def getcolor(self, color, image=None) -> int:
|
||||||
"""Given an rgb tuple, allocate palette entry.
|
"""Given an rgb tuple, allocate palette entry.
|
||||||
|
|
||||||
.. warning:: This method is experimental.
|
.. warning:: This method is experimental.
|
||||||
|
|
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) {
|
_anim_decoder_get_next(PyObject *self) {
|
||||||
uint8_t *buf;
|
uint8_t *buf;
|
||||||
int timestamp;
|
int timestamp;
|
||||||
|
int ok;
|
||||||
PyObject *bytes;
|
PyObject *bytes;
|
||||||
PyObject *ret;
|
PyObject *ret;
|
||||||
|
ImagingSectionCookie cookie;
|
||||||
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
|
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");
|
PyErr_SetString(PyExc_OSError, "failed to read next frame");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
3
tox.ini
3
tox.ini
|
@ -33,14 +33,15 @@ commands =
|
||||||
[testenv:mypy]
|
[testenv:mypy]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
|
-r .ci/requirements-mypy.txt
|
||||||
IceSpringPySideStubs-PyQt6
|
IceSpringPySideStubs-PyQt6
|
||||||
IceSpringPySideStubs-PySide6
|
IceSpringPySideStubs-PySide6
|
||||||
ipython
|
ipython
|
||||||
mypy==1.7.1
|
|
||||||
numpy
|
numpy
|
||||||
packaging
|
packaging
|
||||||
types-cffi
|
types-cffi
|
||||||
types-defusedxml
|
types-defusedxml
|
||||||
|
types-olefile
|
||||||
extras =
|
extras =
|
||||||
typing
|
typing
|
||||||
commands =
|
commands =
|
||||||
|
|
|
@ -308,21 +308,16 @@ DEPS = {
|
||||||
"libs": [r"Lib\MS\*.lib"],
|
"libs": [r"Lib\MS\*.lib"],
|
||||||
},
|
},
|
||||||
"openjpeg": {
|
"openjpeg": {
|
||||||
"url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz",
|
"url": "https://github.com/uclouvain/openjpeg/archive/v2.5.2.tar.gz",
|
||||||
"filename": "openjpeg-2.5.0.tar.gz",
|
"filename": "openjpeg-2.5.2.tar.gz",
|
||||||
"dir": "openjpeg-2.5.0",
|
"dir": "openjpeg-2.5.2",
|
||||||
"license": "LICENSE",
|
"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": [
|
"build": [
|
||||||
*cmds_cmake(
|
*cmds_cmake(
|
||||||
"openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF"
|
"openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF"
|
||||||
),
|
),
|
||||||
cmd_mkdir(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.0"),
|
cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.2"),
|
||||||
],
|
],
|
||||||
"libs": [r"bin\*.lib"],
|
"libs": [r"bin\*.lib"],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user