Merge branch 'main' into init
|
@ -21,9 +21,9 @@ environment:
|
||||||
- PYTHON: C:/Python312
|
- PYTHON: C:/Python312
|
||||||
ARCHITECTURE: x86
|
ARCHITECTURE: x86
|
||||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||||
- PYTHON: C:/Python38-x64
|
- PYTHON: C:/Python39-x64
|
||||||
ARCHITECTURE: AMD64
|
ARCHITECTURE: AMD64
|
||||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||||
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
@ -38,7 +38,7 @@ install:
|
||||||
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
|
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
|
||||||
- cd c:\pillow\winbuild\
|
- cd c:\pillow\winbuild\
|
||||||
- ps: |
|
- ps: |
|
||||||
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||||
c:\pillow\winbuild\build\build_dep_all.cmd
|
c:\pillow\winbuild\build\build_dep_all.cmd
|
||||||
$host.SetShouldExit(0)
|
$host.SetShouldExit(0)
|
||||||
- path C:\pillow\winbuild\build\bin;%PATH%
|
- path C:\pillow\winbuild\build\bin;%PATH%
|
||||||
|
|
|
@ -28,8 +28,6 @@ fi
|
||||||
|
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
python3 -m pip install --upgrade wheel
|
python3 -m pip install --upgrade wheel
|
||||||
# TODO Update condition when cffi supports 3.13
|
|
||||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
|
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
python3 -m pip install defusedxml
|
python3 -m pip install defusedxml
|
||||||
python3 -m pip install olefile
|
python3 -m pip install olefile
|
||||||
|
@ -39,8 +37,7 @@ python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
# TODO Update condition when NumPy supports 3.13
|
python3 -m pip install numpy
|
||||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
|
|
||||||
|
|
||||||
# PyQt6 doesn't support PyPy3
|
# PyQt6 doesn't support PyPy3
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
|
@ -51,7 +48,6 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
# Pyroma uses non-isolated build and fails with old setuptools
|
# Pyroma uses non-isolated build and fails with old setuptools
|
||||||
if [[
|
if [[
|
||||||
$GHA_PYTHON_VERSION == pypy3.9
|
$GHA_PYTHON_VERSION == pypy3.9
|
||||||
|| $GHA_PYTHON_VERSION == 3.8
|
|
||||||
|| $GHA_PYTHON_VERSION == 3.9
|
|| $GHA_PYTHON_VERSION == 3.9
|
||||||
]]; then
|
]]; then
|
||||||
# To match pyproject.toml
|
# To match pyproject.toml
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==2.19.1
|
cibuildwheel==2.19.2
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
mypy==1.10.0
|
mypy==1.10.1
|
||||||
|
|
|
@ -19,6 +19,5 @@ exclude_also =
|
||||||
[run]
|
[run]
|
||||||
omit =
|
omit =
|
||||||
Tests/32bit_segfault_check.py
|
Tests/32bit_segfault_check.py
|
||||||
Tests/bench_cffi_access.py
|
|
||||||
Tests/check_*.py
|
Tests/check_*.py
|
||||||
Tests/createfontdatachunk.py
|
Tests/createfontdatachunk.py
|
||||||
|
|
7
.github/workflows/macos-install.sh
vendored
|
@ -18,9 +18,6 @@ else
|
||||||
fi
|
fi
|
||||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
|
||||||
# TODO Update condition when cffi supports 3.13
|
|
||||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
|
|
||||||
|
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
python3 -m pip install defusedxml
|
python3 -m pip install defusedxml
|
||||||
python3 -m pip install olefile
|
python3 -m pip install olefile
|
||||||
|
@ -28,9 +25,7 @@ python3 -m pip install -U pytest
|
||||||
python3 -m pip install -U pytest-cov
|
python3 -m pip install -U pytest-cov
|
||||||
python3 -m pip install -U pytest-timeout
|
python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
python3 -m pip install numpy
|
||||||
# TODO Update condition when NumPy supports 3.13
|
|
||||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
|
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
3
.github/workflows/test-cygwin.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-minor-version: [8, 9]
|
python-minor-version: [9]
|
||||||
|
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
|
|
||||||
|
@ -72,7 +72,6 @@ jobs:
|
||||||
make
|
make
|
||||||
netpbm
|
netpbm
|
||||||
perl
|
perl
|
||||||
python3${{ matrix.python-minor-version }}-cffi
|
|
||||||
python3${{ matrix.python-minor-version }}-cython
|
python3${{ matrix.python-minor-version }}-cython
|
||||||
python3${{ matrix.python-minor-version }}-devel
|
python3${{ matrix.python-minor-version }}-devel
|
||||||
python3${{ matrix.python-minor-version }}-numpy
|
python3${{ matrix.python-minor-version }}-numpy
|
||||||
|
|
2
.github/workflows/test-docker.yml
vendored
|
@ -44,13 +44,11 @@ jobs:
|
||||||
amazon-2023-amd64,
|
amazon-2023-amd64,
|
||||||
arch,
|
arch,
|
||||||
centos-stream-9-amd64,
|
centos-stream-9-amd64,
|
||||||
debian-11-bullseye-amd64,
|
|
||||||
debian-12-bookworm-x86,
|
debian-12-bookworm-x86,
|
||||||
debian-12-bookworm-amd64,
|
debian-12-bookworm-amd64,
|
||||||
fedora-39-amd64,
|
fedora-39-amd64,
|
||||||
fedora-40-amd64,
|
fedora-40-amd64,
|
||||||
gentoo,
|
gentoo,
|
||||||
ubuntu-20.04-focal-amd64,
|
|
||||||
ubuntu-22.04-jammy-amd64,
|
ubuntu-22.04-jammy-amd64,
|
||||||
ubuntu-24.04-noble-amd64,
|
ubuntu-24.04-noble-amd64,
|
||||||
]
|
]
|
||||||
|
|
1
.github/workflows/test-mingw.yml
vendored
|
@ -64,7 +64,6 @@ jobs:
|
||||||
mingw-w64-x86_64-libtiff \
|
mingw-w64-x86_64-libtiff \
|
||||||
mingw-w64-x86_64-libwebp \
|
mingw-w64-x86_64-libwebp \
|
||||||
mingw-w64-x86_64-openjpeg2 \
|
mingw-w64-x86_64-openjpeg2 \
|
||||||
mingw-w64-x86_64-python3-cffi \
|
|
||||||
mingw-w64-x86_64-python3-numpy \
|
mingw-w64-x86_64-python3-numpy \
|
||||||
mingw-w64-x86_64-python3-olefile \
|
mingw-w64-x86_64-python3-olefile \
|
||||||
mingw-w64-x86_64-python3-setuptools \
|
mingw-w64-x86_64-python3-setuptools \
|
||||||
|
|
2
.github/workflows/test-windows.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
||||||
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.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
|
5
.github/workflows/test.yml
vendored
|
@ -48,7 +48,6 @@ jobs:
|
||||||
"3.11",
|
"3.11",
|
||||||
"3.10",
|
"3.10",
|
||||||
"3.9",
|
"3.9",
|
||||||
"3.8",
|
|
||||||
]
|
]
|
||||||
include:
|
include:
|
||||||
- python-version: "3.11"
|
- python-version: "3.11"
|
||||||
|
@ -59,13 +58,9 @@ jobs:
|
||||||
# M1 only available for 3.10+
|
# M1 only available for 3.10+
|
||||||
- os: "macos-13"
|
- os: "macos-13"
|
||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
- os: "macos-13"
|
|
||||||
python-version: "3.8"
|
|
||||||
exclude:
|
exclude:
|
||||||
- os: "macos-14"
|
- os: "macos-14"
|
||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
- os: "macos-14"
|
|
||||||
python-version: "3.8"
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
2
.github/workflows/wheels-test.sh
vendored
|
@ -12,7 +12,7 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
||||||
else
|
else
|
||||||
yum install -y fribidi
|
yum install -y fribidi
|
||||||
fi
|
fi
|
||||||
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then
|
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
|
||||||
python3 -m pip install numpy
|
python3 -m pip install numpy
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
11
.github/workflows/wheels.yml
vendored
|
@ -41,12 +41,8 @@ jobs:
|
||||||
python-version:
|
python-version:
|
||||||
- pp39
|
- pp39
|
||||||
- pp310
|
- pp310
|
||||||
- cp38
|
- cp3{9,10,11}
|
||||||
- cp39
|
- cp3{12,13}
|
||||||
- cp310
|
|
||||||
- cp311
|
|
||||||
- cp312
|
|
||||||
- cp313
|
|
||||||
spec:
|
spec:
|
||||||
- manylinux2014
|
- manylinux2014
|
||||||
- manylinux_2_28
|
- manylinux_2_28
|
||||||
|
@ -136,8 +132,6 @@ jobs:
|
||||||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
CIBW_PRERELEASE_PYTHONS: True
|
CIBW_PRERELEASE_PYTHONS: True
|
||||||
CIBW_SKIP: pp38-*
|
|
||||||
CIBW_TEST_SKIP: cp38-macosx_arm64
|
|
||||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
|
@ -208,7 +202,6 @@ jobs:
|
||||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||||
CIBW_CACHE_PATH: "C:\\cibw"
|
CIBW_CACHE_PATH: "C:\\cibw"
|
||||||
CIBW_PRERELEASE_PYTHONS: True
|
CIBW_PRERELEASE_PYTHONS: True
|
||||||
CIBW_SKIP: pp38-*
|
|
||||||
CIBW_TEST_SKIP: "*-win_arm64"
|
CIBW_TEST_SKIP: "*-win_arm64"
|
||||||
CIBW_TEST_COMMAND: 'docker run --rm
|
CIBW_TEST_COMMAND: 'docker run --rm
|
||||||
-v {project}:C:\pillow
|
-v {project}:C:\pillow
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.4.7
|
rev: v0.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--exit-non-zero-on-fix]
|
args: [--exit-non-zero-on-fix]
|
||||||
|
@ -11,7 +11,7 @@ repos:
|
||||||
- id: black
|
- id: black
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
rev: 1.7.8
|
rev: 1.7.9
|
||||||
hooks:
|
hooks:
|
||||||
- id: bandit
|
- id: bandit
|
||||||
args: [--severity-level=high]
|
args: [--severity-level=high]
|
||||||
|
@ -24,7 +24,7 @@ repos:
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v18.1.5
|
rev: v18.1.8
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
types: [c]
|
types: [c]
|
||||||
|
@ -50,7 +50,7 @@ repos:
|
||||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||||
|
|
||||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||||
rev: 0.28.4
|
rev: 0.28.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-github-workflows
|
- id: check-github-workflows
|
||||||
- id: check-readthedocs
|
- id: check-readthedocs
|
||||||
|
@ -62,7 +62,7 @@ repos:
|
||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: 1.8.0
|
rev: 2.1.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
|
|
||||||
|
|
92
CHANGES.rst
|
@ -2,9 +2,99 @@
|
||||||
Changelog (Pillow)
|
Changelog (Pillow)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
10.4.0 (unreleased)
|
11.0.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Drop support for Python 3.8 #8183
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
- Add support for Python 3.13 #8181
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
- Fix incompatibility with NumPy 1.20 #8187
|
||||||
|
[neutrinoceros, radarhere]
|
||||||
|
|
||||||
|
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
10.4.0 (2024-07-01)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Raise FileNotFoundError if show_file() path does not exist #8178
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improved reading 16-bit TGA images with colour #7965
|
||||||
|
[Yay295, radarhere]
|
||||||
|
|
||||||
|
- Deprecate non-image ImageCms modes #8031
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed processing multiple JPEG EXIF markers #8127
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not preserve EXIFIFD tag by default when saving TIFF images #8110
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added ImageFont.load_default_imagefont() #8086
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added Image.WARN_POSSIBLE_FORMATS #8063
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Remove zero-byte end padding when parsing any XMP data #8171
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not detect Ultra HDR images as MPO #8056
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Raise SyntaxError specific to JP2 #8146
|
||||||
|
[Yay295, radarhere]
|
||||||
|
|
||||||
|
- Do not use first frame duration for other frames when saving APNG images #8104
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Consider I;16 pixel size when using a 1 mode mask #8112
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- When saving multiple PNG frames, convert to mode rather than raw mode #8087
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added byte support to FreeTypeFont #8141
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Allow float center for rotate operations #8114
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not read layers immediately when opening PSD images #8039
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Restore original thread state #8065
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Read IM and TIFF images as RGB, rather than RGBX #7997
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Clarify ImageDraw2 error message when size is missing #8165
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support unpacking more rawmodes to RGBA palettes #7966
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Removed support for Qt 5 #8159
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
|
||||||
|
[mamg22, radarhere]
|
||||||
|
|
||||||
|
- Improved consistency of XMP handling #8069
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Use pkg-config to help find libwebp and raqm #8142
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Accept 't' suffix for libtiff version #8126, #8129
|
- Accept 't' suffix for libtiff version #8126, #8129
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
from PIL import PyAccess
|
|
||||||
|
|
||||||
from .helper import hopper
|
|
||||||
|
|
||||||
# Not running this test by default. No DOS against CI.
|
|
||||||
|
|
||||||
|
|
||||||
def iterate_get(size, access) -> None:
|
|
||||||
(w, h) = size
|
|
||||||
for x in range(w):
|
|
||||||
for y in range(h):
|
|
||||||
access[(x, y)]
|
|
||||||
|
|
||||||
|
|
||||||
def iterate_set(size, access) -> None:
|
|
||||||
(w, h) = size
|
|
||||||
for x in range(w):
|
|
||||||
for y in range(h):
|
|
||||||
access[(x, y)] = (x % 256, y % 256, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def timer(func, label, *args) -> None:
|
|
||||||
iterations = 5000
|
|
||||||
starttime = time.time()
|
|
||||||
for x in range(iterations):
|
|
||||||
func(*args)
|
|
||||||
if time.time() - starttime > 10:
|
|
||||||
break
|
|
||||||
endtime = time.time()
|
|
||||||
print(
|
|
||||||
f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
|
|
||||||
f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_direct() -> None:
|
|
||||||
im = hopper()
|
|
||||||
im.load()
|
|
||||||
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
|
|
||||||
caccess = im.im.pixel_access(False)
|
|
||||||
access = PyAccess.new(im, False)
|
|
||||||
|
|
||||||
assert access is not None
|
|
||||||
assert caccess[(0, 0)] == access[(0, 0)]
|
|
||||||
|
|
||||||
print(f"Size: {im.width}x{im.height}")
|
|
||||||
timer(iterate_get, "PyAccess - get", im.size, access)
|
|
||||||
timer(iterate_set, "PyAccess - set", im.size, access)
|
|
||||||
timer(iterate_get, "C-api - get", im.size, caccess)
|
|
||||||
timer(iterate_set, "C-api - set", im.size, caccess)
|
|
|
@ -11,14 +11,15 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from collections.abc import Sequence
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Callable, Sequence
|
from typing import Any, Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image, ImageMath, features
|
from PIL import Image, ImageFile, ImageMath, features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -59,9 +60,7 @@ def convert_to_comparable(
|
||||||
return new_a, new_b
|
return new_a, new_b
|
||||||
|
|
||||||
|
|
||||||
def assert_deep_equal(
|
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
|
||||||
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
|
|
||||||
) -> None:
|
|
||||||
try:
|
try:
|
||||||
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
|
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -240,7 +239,7 @@ class PillowLeakTestCase:
|
||||||
# helpers
|
# helpers
|
||||||
|
|
||||||
|
|
||||||
def fromstring(data: bytes) -> Image.Image:
|
def fromstring(data: bytes) -> ImageFile.ImageFile:
|
||||||
return Image.open(BytesIO(data))
|
return Image.open(BytesIO(data))
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
Tests/images/imagedraw_polygon_width_I.tiff
Normal file
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 391 B |
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 414 B |
BIN
Tests/images/rgba16.tga
Normal file
After Width: | Height: | Size: 48 B |
BIN
Tests/images/ultrahdr.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
Tests/images/unknown_mode.j2k
Normal file
|
@ -321,6 +321,7 @@ class TestColorLut3DCoreAPI:
|
||||||
-1, 2, 2, 2, 2, 2,
|
-1, 2, 2, 2, 2, 2,
|
||||||
])).load()
|
])).load()
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
assert transformed is not None
|
||||||
assert transformed[0, 0] == (0, 0, 255)
|
assert transformed[0, 0] == (0, 0, 255)
|
||||||
assert transformed[50, 50] == (0, 0, 255)
|
assert transformed[50, 50] == (0, 0, 255)
|
||||||
assert transformed[255, 0] == (0, 255, 255)
|
assert transformed[255, 0] == (0, 255, 255)
|
||||||
|
@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
|
||||||
-3, 5, 5, 5, 5, 5,
|
-3, 5, 5, 5, 5, 5,
|
||||||
])).load()
|
])).load()
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
assert transformed is not None
|
||||||
assert transformed[0, 0] == (0, 0, 255)
|
assert transformed[0, 0] == (0, 0, 255)
|
||||||
assert transformed[50, 50] == (0, 0, 255)
|
assert transformed[50, 50] == (0, 0, 255)
|
||||||
assert transformed[255, 0] == (0, 255, 255)
|
assert transformed[255, 0] == (0, 255, 255)
|
||||||
|
@ -354,10 +356,10 @@ class TestColorLut3DCoreAPI:
|
||||||
class TestColorLut3DFilter:
|
class TestColorLut3DFilter:
|
||||||
def test_wrong_args(self) -> None:
|
def test_wrong_args(self) -> None:
|
||||||
with pytest.raises(ValueError, match="should be either an integer"):
|
with pytest.raises(ValueError, match="should be either an integer"):
|
||||||
ImageFilter.Color3DLUT("small", [1])
|
ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="should be either an integer"):
|
with pytest.raises(ValueError, match="should be either an integer"):
|
||||||
ImageFilter.Color3DLUT((11, 11), [1])
|
ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
|
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
|
||||||
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
||||||
|
|
|
@ -9,9 +9,9 @@ from PIL import _deprecate
|
||||||
"version, expected",
|
"version, expected",
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
11,
|
12,
|
||||||
"Old thing is deprecated and will be removed in Pillow 11 "
|
"Old thing is deprecated and will be removed in Pillow 12 "
|
||||||
r"\(2024-10-15\)\. Use new thing instead\.",
|
r"\(2025-10-15\)\. Use new thing instead\.",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
|
@ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
|
||||||
|
|
||||||
def test_plural() -> None:
|
def test_plural() -> None:
|
||||||
expected = (
|
expected = (
|
||||||
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
|
||||||
r"Use new thing instead\."
|
r"Use new thing instead\."
|
||||||
)
|
)
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
|
_deprecate.deprecate("Old things", 12, "new thing", plural=True)
|
||||||
|
|
||||||
|
|
||||||
def test_replacement_and_action() -> None:
|
def test_replacement_and_action() -> None:
|
||||||
expected = "Use only one of 'replacement' and 'action'"
|
expected = "Use only one of 'replacement' and 'action'"
|
||||||
with pytest.raises(ValueError, match=expected):
|
with pytest.raises(ValueError, match=expected):
|
||||||
_deprecate.deprecate(
|
_deprecate.deprecate(
|
||||||
"Old thing", 11, replacement="new thing", action="Upgrade to new thing"
|
"Old thing", 12, replacement="new thing", action="Upgrade to new thing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,16 +78,16 @@ def test_replacement_and_action() -> None:
|
||||||
)
|
)
|
||||||
def test_action(action: str) -> 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 12 \(2025-10-15\)\. "
|
||||||
r"Upgrade to new thing\."
|
r"Upgrade to new thing\."
|
||||||
)
|
)
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old thing", 11, action=action)
|
_deprecate.deprecate("Old thing", 12, action=action)
|
||||||
|
|
||||||
|
|
||||||
def test_no_replacement_or_action() -> None:
|
def test_no_replacement_or_action() -> 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 12 \(2025-10-15\)"
|
||||||
)
|
)
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old thing", 11)
|
_deprecate.deprecate("Old thing", 12)
|
||||||
|
|
|
@ -706,10 +706,21 @@ def test_different_modes_in_later_frames(
|
||||||
assert reloaded.mode == mode
|
assert reloaded.mode == mode
|
||||||
|
|
||||||
|
|
||||||
def test_apng_repeated_seeks_give_correct_info() -> None:
|
def test_different_durations(tmp_path: Path) -> None:
|
||||||
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/different_durations.png") as im:
|
with Image.open("Tests/images/apng/different_durations.png") as im:
|
||||||
for i in range(3):
|
for _ in range(3):
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
assert im.info["duration"] == 4000
|
assert im.info["duration"] == 4000
|
||||||
|
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.info["duration"] == 1000
|
assert im.info["duration"] == 1000
|
||||||
|
|
||||||
|
im.save(test_file, save_all=True)
|
||||||
|
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert reloaded.info["duration"] == 4000
|
||||||
|
|
||||||
|
reloaded.seek(1)
|
||||||
|
assert reloaded.info["duration"] == 1000
|
||||||
|
|
|
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
def is_loaded(self) -> bool:
|
||||||
|
return self.loaded
|
||||||
|
|
||||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
|
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
BufrStubImagePlugin.register_handler(handler)
|
BufrStubImagePlugin.register_handler(handler)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert handler.opened
|
assert handler.opened
|
||||||
assert not handler.loaded
|
assert not handler.is_loaded()
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
assert handler.loaded
|
assert handler.is_loaded()
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.bufr")
|
temp_file = str(tmp_path / "temp.bufr")
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
|
@ -329,46 +329,6 @@ def test_read_binary_preview() -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_readline_psfile(tmp_path: Path) -> None:
|
|
||||||
# check all the freaking line endings possible from the spec
|
|
||||||
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
|
|
||||||
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
|
||||||
strings = ["something", "else", "baz", "bif"]
|
|
||||||
|
|
||||||
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
|
|
||||||
ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
|
|
||||||
assert t.readline().strip("\r\n") == "something", ending
|
|
||||||
assert t.readline().strip("\r\n") == "else", ending
|
|
||||||
assert t.readline().strip("\r\n") == "baz", ending
|
|
||||||
assert t.readline().strip("\r\n") == "bif", ending
|
|
||||||
|
|
||||||
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
|
|
||||||
f = io.BytesIO(test_string.encode("latin-1"))
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
t = EpsImagePlugin.PSFile(f)
|
|
||||||
_test_readline(t, ending)
|
|
||||||
|
|
||||||
def _test_readline_file_psfile(test_string: str, ending: str) -> None:
|
|
||||||
f = str(tmp_path / "temp.txt")
|
|
||||||
with open(f, "wb") as w:
|
|
||||||
w.write(test_string.encode("latin-1"))
|
|
||||||
|
|
||||||
with open(f, "rb") as r:
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
t = EpsImagePlugin.PSFile(r)
|
|
||||||
_test_readline(t, ending)
|
|
||||||
|
|
||||||
for ending in line_endings:
|
|
||||||
s = ending.join(strings)
|
|
||||||
_test_readline_io_psfile(s, ending)
|
|
||||||
_test_readline_file_psfile(s, ending)
|
|
||||||
|
|
||||||
|
|
||||||
def test_psfile_deprecation() -> None:
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
EpsImagePlugin.PSFile(None)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"line_ending",
|
"line_ending",
|
||||||
|
@ -425,9 +385,10 @@ def test_timeout(test_file: str) -> None:
|
||||||
def test_bounding_box_in_trailer() -> None:
|
def test_bounding_box_in_trailer() -> None:
|
||||||
# Check bounding boxes are parsed in the same way
|
# Check bounding boxes are parsed in the same way
|
||||||
# when specified in the header and the trailer
|
# when specified in the header and the trailer
|
||||||
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
|
with (
|
||||||
FILE1
|
Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
|
||||||
) as header_image:
|
Image.open(FILE1) as header_image,
|
||||||
|
):
|
||||||
assert trailer_image.size == header_image.size
|
assert trailer_image.size == header_image.size
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
from collections.abc import Generator
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -353,7 +353,7 @@ def test_palette_434(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
|
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im.copy().save(out, **kwargs)
|
im.copy().save(out, "GIF", **kwargs)
|
||||||
reloaded = Image.open(out)
|
reloaded = Image.open(out)
|
||||||
|
|
||||||
return reloaded
|
return reloaded
|
||||||
|
|
|
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
def is_loaded(self) -> bool:
|
||||||
|
return self.loaded
|
||||||
|
|
||||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
|
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
GribStubImagePlugin.register_handler(handler)
|
GribStubImagePlugin.register_handler(handler)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert handler.opened
|
assert handler.opened
|
||||||
assert not handler.loaded
|
assert not handler.is_loaded()
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
assert handler.loaded
|
assert handler.is_loaded()
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.grib")
|
temp_file = str(tmp_path / "temp.grib")
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
|
@ -66,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
def is_loaded(self) -> bool:
|
||||||
|
return self.loaded
|
||||||
|
|
||||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
|
@ -73,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
Hdf5StubImagePlugin.register_handler(handler)
|
Hdf5StubImagePlugin.register_handler(handler)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert handler.opened
|
assert handler.opened
|
||||||
assert not handler.loaded
|
assert not handler.is_loaded()
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
assert handler.loaded
|
assert handler.is_loaded()
|
||||||
|
|
||||||
temp_file = str(tmp_path / "temp.h5")
|
temp_file = str(tmp_path / "temp.h5")
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
|
@ -872,7 +872,7 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_multiple_exif(self) -> None:
|
def test_multiple_exif(self) -> None:
|
||||||
with Image.open("Tests/images/multiple_exif.jpg") as im:
|
with Image.open("Tests/images/multiple_exif.jpg") as im:
|
||||||
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
|
assert im.getexif()[270] == "firstsecond"
|
||||||
|
|
||||||
@mark_if_feature_version(
|
@mark_if_feature_version(
|
||||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||||
|
@ -948,6 +948,7 @@ class TestFileJpeg:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
||||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
@ -1032,8 +1033,10 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_repr_jpeg(self) -> None:
|
def test_repr_jpeg(self) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
b = im._repr_jpeg_()
|
||||||
|
assert b is not None
|
||||||
|
|
||||||
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
|
with Image.open(BytesIO(b)) as repr_jpeg:
|
||||||
assert repr_jpeg.format == "JPEG"
|
assert repr_jpeg.format == "JPEG"
|
||||||
assert_image_similar(im, repr_jpeg, 17)
|
assert_image_similar(im, repr_jpeg, 17)
|
||||||
|
|
||||||
|
|
|
@ -335,9 +335,15 @@ def test_issue_6194() -> None:
|
||||||
assert im.getpixel((5, 5)) == 31
|
assert im.getpixel((5, 5)) == 31
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_j2k_mode() -> None:
|
||||||
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
with Image.open("Tests/images/unknown_mode.j2k"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_unbound_local() -> None:
|
def test_unbound_local() -> None:
|
||||||
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
with Image.open("Tests/images/unbound_variable.jp2"):
|
with Image.open("Tests/images/unbound_variable.jp2"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
|
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
|
||||||
"""Testing loading from non-disk non-BytesIO file object"""
|
"""Testing loading from non-disk non-BytesIO file object"""
|
||||||
test_file = "Tests/images/hopper_g4_500.tif"
|
test_file = "Tests/images/hopper_g4_500.tif"
|
||||||
s = io.BytesIO()
|
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
s.write(f.read())
|
data = f.read()
|
||||||
s.seek(0)
|
|
||||||
r = io.BufferedReader(s)
|
class NonBytesIO(io.RawIOBase):
|
||||||
|
def read(self, size: int = -1) -> bytes:
|
||||||
|
nonlocal data
|
||||||
|
if size == -1:
|
||||||
|
size = len(data)
|
||||||
|
result = data[:size]
|
||||||
|
data = data[size:]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def readable(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
r = io.BufferedReader(NonBytesIO())
|
||||||
with Image.open(r) as im:
|
with Image.open(r) as im:
|
||||||
assert im.size == (500, 500)
|
assert im.size == (500, 500)
|
||||||
self._assert_noerr(tmp_path, im)
|
self._assert_noerr(tmp_path, im)
|
||||||
|
@ -685,13 +696,18 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert reloaded.tag_v2[530] == (1, 1)
|
assert reloaded.tag_v2[530] == (1, 1)
|
||||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||||
|
|
||||||
def test_exif_ifd(self, tmp_path: Path) -> None:
|
def test_exif_ifd(self) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
out = io.BytesIO()
|
||||||
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
||||||
assert im.tag_v2[34665] == 125456
|
assert im.tag_v2[34665] == 125456
|
||||||
im.save(outfile)
|
im.save(out, "TIFF")
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert 34665 not in reloaded.tag_v2
|
||||||
|
|
||||||
|
im.save(out, "TIFF", tiffinfo={34665: 125456})
|
||||||
|
|
||||||
|
with Image.open(out) as reloaded:
|
||||||
if Image.core.libtiff_support_custom_tags:
|
if Image.core.libtiff_support_custom_tags:
|
||||||
assert reloaded.tag_v2[34665] == 125456
|
assert reloaded.tag_v2[34665] == 125456
|
||||||
|
|
||||||
|
@ -1043,7 +1059,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_wrong_bits_per_sample(
|
def test_wrong_bits_per_sample(
|
||||||
self, file_name: str, mode: str, size: tuple[int, int], tile
|
self,
|
||||||
|
file_name: str,
|
||||||
|
mode: str,
|
||||||
|
size: tuple[int, int],
|
||||||
|
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
|
||||||
) -> None:
|
) -> None:
|
||||||
with Image.open("Tests/images/" + file_name) as im:
|
with Image.open("Tests/images/" + file_name) as im:
|
||||||
assert im.mode == mode
|
assert im.mode == mode
|
||||||
|
@ -1130,7 +1150,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
|
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
|
||||||
if argument:
|
if argument:
|
||||||
arguments["strip_size"] = 2**18
|
arguments["strip_size"] = 2**18
|
||||||
im.save(out, **arguments)
|
im.save(out, "TIFF", **arguments)
|
||||||
|
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
|
@ -226,6 +226,11 @@ def test_eoferror() -> None:
|
||||||
im.seek(n_frames - 1)
|
im.seek(n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ultra_hdr() -> None:
|
||||||
|
with Image.open("Tests/images/ultrahdr.jpg") as im:
|
||||||
|
assert im.format == "JPEG"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_file", test_files)
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
def test_image_grab(test_file: str) -> None:
|
def test_image_grab(test_file: str) -> None:
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
|
|
@ -76,6 +76,7 @@ def test_pil184() -> None:
|
||||||
def test_1px_width(tmp_path: Path) -> None:
|
def test_1px_width(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (1, 256))
|
im = Image.new("L", (1, 256))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(256):
|
for y in range(256):
|
||||||
px[0, y] = y
|
px[0, y] = y
|
||||||
_roundtrip(tmp_path, im)
|
_roundtrip(tmp_path, im)
|
||||||
|
@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
|
||||||
def test_large_count(tmp_path: Path) -> None:
|
def test_large_count(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (256, 1))
|
im = Image.new("L", (256, 1))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, 0] = x // 67 * 67
|
px[x, 0] = x // 67 * 67
|
||||||
_roundtrip(tmp_path, im)
|
_roundtrip(tmp_path, im)
|
||||||
|
@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) ->
|
||||||
def test_break_in_count_overflow(tmp_path: Path) -> None:
|
def test_break_in_count_overflow(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (256, 5))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(4):
|
for y in range(4):
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
|
@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
|
||||||
def test_break_one_in_loop(tmp_path: Path) -> None:
|
def test_break_one_in_loop(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (256, 5))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
|
@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
|
||||||
def test_break_many_in_loop(tmp_path: Path) -> None:
|
def test_break_many_in_loop(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (256, 5))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(4):
|
for y in range(4):
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
|
@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
|
||||||
def test_break_one_at_end(tmp_path: Path) -> None:
|
def test_break_one_at_end(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (256, 5))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
|
@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
|
||||||
def test_break_many_at_end(tmp_path: Path) -> None:
|
def test_break_many_at_end(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (256, 5))
|
im = Image.new("L", (256, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(256):
|
for x in range(256):
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
|
@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
|
||||||
def test_break_padding(tmp_path: Path) -> None:
|
def test_break_padding(tmp_path: Path) -> None:
|
||||||
im = Image.new("L", (257, 5))
|
im = Image.new("L", (257, 5))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(5):
|
for y in range(5):
|
||||||
for x in range(257):
|
for x in range(257):
|
||||||
px[x, y] = x % 128
|
px[x, y] = x % 128
|
||||||
|
|
|
@ -5,8 +5,9 @@ import os
|
||||||
import os.path
|
import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Generator
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@ 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")
|
||||||
im.save(outfile, **params)
|
im.save(outfile, "PDF", **params)
|
||||||
|
|
||||||
with open(outfile, "rb") as fp:
|
with open(outfile, "rb") as fp:
|
||||||
contents = fp.read()
|
contents = fp.read()
|
||||||
|
|
|
@ -535,8 +535,10 @@ class TestFilePng:
|
||||||
|
|
||||||
def test_repr_png(self) -> None:
|
def test_repr_png(self) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
b = im._repr_png_()
|
||||||
|
assert b is not None
|
||||||
|
|
||||||
with Image.open(BytesIO(im._repr_png_())) as repr_png:
|
with Image.open(BytesIO(b)) as repr_png:
|
||||||
assert repr_png.format == "PNG"
|
assert repr_png.format == "PNG"
|
||||||
assert_image_equal(im, repr_png)
|
assert_image_equal(im, repr_png)
|
||||||
|
|
||||||
|
@ -655,11 +657,12 @@ class TestFilePng:
|
||||||
png.call(cid, 0, 0)
|
png.call(cid, 0, 0)
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
def test_specify_bits(self, tmp_path: Path) -> None:
|
@pytest.mark.parametrize("save_all", (True, False))
|
||||||
|
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
|
|
||||||
out = str(tmp_path / "temp.png")
|
out = str(tmp_path / "temp.png")
|
||||||
im.save(out, bits=4)
|
im.save(out, bits=4, save_all=save_all)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert len(reloaded.png.im_palette[1]) == 48
|
assert len(reloaded.png.im_palette[1]) == 48
|
||||||
|
@ -683,6 +686,7 @@ class TestFilePng:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
||||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
@ -767,16 +771,12 @@ class TestFilePng:
|
||||||
def test_save_stdout(self, buffer: bool) -> None:
|
def test_save_stdout(self, buffer: bool) -> None:
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
|
|
||||||
if buffer:
|
class MyStdOut:
|
||||||
|
buffer = BytesIO()
|
||||||
|
|
||||||
class MyStdOut:
|
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||||
buffer = BytesIO()
|
|
||||||
|
|
||||||
mystdout = MyStdOut()
|
sys.stdout = mystdout # type: ignore[assignment]
|
||||||
else:
|
|
||||||
mystdout = BytesIO()
|
|
||||||
|
|
||||||
sys.stdout = mystdout
|
|
||||||
|
|
||||||
with Image.open(TEST_PNG_FILE) as im:
|
with Image.open(TEST_PNG_FILE) as im:
|
||||||
im.save(sys.stdout, "PNG")
|
im.save(sys.stdout, "PNG")
|
||||||
|
@ -784,7 +784,7 @@ class TestFilePng:
|
||||||
# Reset stdout
|
# Reset stdout
|
||||||
sys.stdout = old_stdout
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
if buffer:
|
if isinstance(mystdout, MyStdOut):
|
||||||
mystdout = mystdout.buffer
|
mystdout = mystdout.buffer
|
||||||
with Image.open(mystdout) as reloaded:
|
with Image.open(mystdout) as reloaded:
|
||||||
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
||||||
|
|
|
@ -368,16 +368,12 @@ def test_mimetypes(tmp_path: Path) -> None:
|
||||||
def test_save_stdout(buffer: bool) -> None:
|
def test_save_stdout(buffer: bool) -> None:
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
|
|
||||||
if buffer:
|
class MyStdOut:
|
||||||
|
buffer = BytesIO()
|
||||||
|
|
||||||
class MyStdOut:
|
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||||
buffer = BytesIO()
|
|
||||||
|
|
||||||
mystdout = MyStdOut()
|
sys.stdout = mystdout # type: ignore[assignment]
|
||||||
else:
|
|
||||||
mystdout = BytesIO()
|
|
||||||
|
|
||||||
sys.stdout = mystdout
|
|
||||||
|
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
im.save(sys.stdout, "PPM")
|
im.save(sys.stdout, "PPM")
|
||||||
|
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
|
||||||
# Reset stdout
|
# Reset stdout
|
||||||
sys.stdout = old_stdout
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
if buffer:
|
if isinstance(mystdout, MyStdOut):
|
||||||
mystdout = mystdout.buffer
|
mystdout = mystdout.buffer
|
||||||
with Image.open(mystdout) as reloaded:
|
with Image.open(mystdout) as reloaded:
|
||||||
assert_image_equal_tofile(reloaded, TEST_FILE)
|
assert_image_equal_tofile(reloaded, TEST_FILE)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, PsdImagePlugin, UnidentifiedImageError
|
from PIL import Image, PsdImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
||||||
|
|
||||||
|
@ -150,20 +150,26 @@ def test_combined_larger_than_size() -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file,raises",
|
"test_file,raises",
|
||||||
[
|
[
|
||||||
(
|
|
||||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
|
||||||
UnidentifiedImageError,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
|
||||||
UnidentifiedImageError,
|
|
||||||
),
|
|
||||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
||||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crashes(test_file: str, raises) -> None:
|
def test_crashes(test_file: str, raises: type[Exception]) -> 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):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"test_file",
|
||||||
|
[
|
||||||
|
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||||
|
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_layer_crashes(test_file: str) -> None:
|
||||||
|
with open(test_file, "rb") as f:
|
||||||
|
with Image.open(f) as im:
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
im.layers
|
||||||
|
|
|
@ -105,6 +105,7 @@ def test_load_image_series() -> None:
|
||||||
img_list = SpiderImagePlugin.loadImageSeries(file_list)
|
img_list = SpiderImagePlugin.loadImageSeries(file_list)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
assert img_list is not None
|
||||||
assert len(img_list) == 1
|
assert len(img_list) == 1
|
||||||
assert isinstance(img_list[0], Image.Image)
|
assert isinstance(img_list[0], Image.Image)
|
||||||
assert img_list[0].size == (128, 128)
|
assert img_list[0].size == (128, 128)
|
||||||
|
|
|
@ -72,12 +72,21 @@ def test_palette_depth_8(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def test_palette_depth_16(tmp_path: Path) -> None:
|
def test_palette_depth_16(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/p_16.tga") as im:
|
with Image.open("Tests/images/p_16.tga") as im:
|
||||||
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
|
assert im.palette.mode == "RGBA"
|
||||||
|
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
|
||||||
|
|
||||||
out = str(tmp_path / "temp.png")
|
out = str(tmp_path / "temp.png")
|
||||||
im.save(out)
|
im.save(out)
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
|
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_rgba_16() -> None:
|
||||||
|
with Image.open("Tests/images/rgba16.tga") as im:
|
||||||
|
assert im.mode == "RGBA"
|
||||||
|
|
||||||
|
assert im.getpixel((0, 0)) == (172, 0, 255, 255)
|
||||||
|
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_id_field() -> None:
|
def test_id_field() -> None:
|
||||||
|
|
|
@ -2,10 +2,10 @@ from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
from collections.abc import Generator
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -78,6 +78,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_seek_after_close(self) -> None:
|
def test_seek_after_close(self) -> None:
|
||||||
im = Image.open("Tests/images/multipage.tiff")
|
im = Image.open("Tests/images/multipage.tiff")
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -120,7 +121,7 @@ class TestFileTiff:
|
||||||
def test_set_legacy_api(self) -> None:
|
def test_set_legacy_api(self) -> None:
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
ifd.legacy_api = None
|
ifd.legacy_api = False
|
||||||
assert str(e.value) == "Not allowing setting of legacy api"
|
assert str(e.value) == "Not allowing setting of legacy api"
|
||||||
|
|
||||||
def test_xyres_tiff(self) -> None:
|
def test_xyres_tiff(self) -> None:
|
||||||
|
@ -424,13 +425,13 @@ class TestFileTiff:
|
||||||
def test_load_float(self) -> None:
|
def test_load_float(self) -> None:
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
data = b"abcdabcd"
|
data = b"abcdabcd"
|
||||||
ret = ifd.load_float(data, False)
|
ret = getattr(ifd, "load_float")(data, False)
|
||||||
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
|
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
|
||||||
|
|
||||||
def test_load_double(self) -> None:
|
def test_load_double(self) -> None:
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
data = b"abcdefghabcdefgh"
|
data = b"abcdefghabcdefgh"
|
||||||
ret = ifd.load_double(data, False)
|
ret = getattr(ifd, "load_double")(data, False)
|
||||||
assert ret == (8.540883223036124e194, 8.540883223036124e194)
|
assert ret == (8.540883223036124e194, 8.540883223036124e194)
|
||||||
|
|
||||||
def test_ifd_tag_type(self) -> None:
|
def test_ifd_tag_type(self) -> None:
|
||||||
|
@ -599,7 +600,7 @@ class TestFileTiff:
|
||||||
def test_with_underscores(self, tmp_path: Path) -> None:
|
def test_with_underscores(self, tmp_path: Path) -> None:
|
||||||
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
|
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
|
||||||
filename = str(tmp_path / "temp.tif")
|
filename = str(tmp_path / "temp.tif")
|
||||||
hopper("RGB").save(filename, **kwargs)
|
hopper("RGB").save(filename, "TIFF", **kwargs)
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
# legacy interface
|
# legacy interface
|
||||||
assert im.tag[X_RESOLUTION][0][0] == 72
|
assert im.tag[X_RESOLUTION][0][0] == 72
|
||||||
|
@ -621,6 +622,22 @@ class TestFileTiff:
|
||||||
|
|
||||||
assert_image_equal_tofile(im, tmpfile)
|
assert_image_equal_tofile(im, tmpfile)
|
||||||
|
|
||||||
|
def test_iptc(self, tmp_path: Path) -> None:
|
||||||
|
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
|
||||||
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
with Image.open("Tests/images/hopper.tif") as im:
|
||||||
|
im.load()
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
ifd[33723] = 1
|
||||||
|
ifd.tagtype[33723] = 4
|
||||||
|
im.tag_v2 = ifd
|
||||||
|
im.save(outfile)
|
||||||
|
|
||||||
|
with Image.open(outfile) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
assert 33723 not in im.tag_v2
|
||||||
|
|
||||||
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
@ -759,6 +776,7 @@ class TestFileTiff:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
||||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||||
|
|
|
@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
|
||||||
|
|
||||||
from .helper import assert_deep_equal, hopper
|
from .helper import assert_deep_equal, hopper
|
||||||
|
|
||||||
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
|
TAG_IDS: dict[str, int] = {
|
||||||
|
info.name: info.value
|
||||||
|
for info in TiffTags.TAGS_V2.values()
|
||||||
|
if info.value is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_rt_metadata(tmp_path: Path) -> None:
|
def test_rt_metadata(tmp_path: Path) -> None:
|
||||||
|
@ -411,8 +415,8 @@ def test_empty_values() -> None:
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
||||||
info.load(data)
|
info.load(data)
|
||||||
# Should not raise ValueError.
|
# Should not raise ValueError.
|
||||||
info = dict(info)
|
info_dict = dict(info)
|
||||||
assert 33432 in info
|
assert 33432 in info_dict
|
||||||
|
|
||||||
|
|
||||||
def test_photoshop_info(tmp_path: Path) -> None:
|
def test_photoshop_info(tmp_path: Path) -> None:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -70,7 +71,9 @@ class TestFileWebp:
|
||||||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||||
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
||||||
|
|
||||||
def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
|
def _roundtrip(
|
||||||
|
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
|
||||||
|
) -> None:
|
||||||
temp_file = str(tmp_path / "temp.webp")
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
|
|
||||||
hopper(mode).save(temp_file, **args)
|
hopper(mode).save(temp_file, **args)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -96,7 +97,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
||||||
check(temp_file1)
|
check(temp_file1)
|
||||||
|
|
||||||
# Tests appending using a generator
|
# Tests 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
|
||||||
|
|
||||||
temp_file2 = str(tmp_path / "temp_generator.webp")
|
temp_file2 = str(tmp_path / "temp_generator.webp")
|
||||||
|
|
|
@ -129,6 +129,7 @@ def test_getxmp() -> None:
|
||||||
):
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
|
assert "xmp" in im.info
|
||||||
assert (
|
assert (
|
||||||
im.getxmp()["xmpmeta"]["xmptk"]
|
im.getxmp()["xmpmeta"]["xmptk"]
|
||||||
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
||||||
|
|
|
@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
|
||||||
|
|
||||||
def test_leak(self) -> None:
|
def test_leak(self) -> None:
|
||||||
if features.check_module("freetype2"):
|
if features.check_module("freetype2"):
|
||||||
ImageFont.core = _util.DeferredError(ImportError)
|
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||||
try:
|
try:
|
||||||
default_font = ImageFont.load_default()
|
default_font = ImageFont.load_default()
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -8,7 +8,8 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO
|
from types import ModuleType
|
||||||
|
from typing import IO, Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -35,6 +36,12 @@ from .helper import (
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ElementTree: ModuleType | None
|
||||||
|
try:
|
||||||
|
from defusedxml import ElementTree
|
||||||
|
except ImportError:
|
||||||
|
ElementTree = None
|
||||||
|
|
||||||
|
|
||||||
# Deprecation helper
|
# Deprecation helper
|
||||||
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
|
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
|
||||||
|
@ -129,6 +136,15 @@ class TestImage:
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
|
||||||
|
def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)
|
||||||
|
|
||||||
|
im = io.BytesIO(b"")
|
||||||
|
with pytest.warns(UserWarning):
|
||||||
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
with Image.open(im):
|
||||||
|
pass
|
||||||
|
|
||||||
def test_width_height(self) -> None:
|
def test_width_height(self) -> None:
|
||||||
im = Image.new("RGB", (1, 2))
|
im = Image.new("RGB", (1, 2))
|
||||||
assert im.width == 1
|
assert im.width == 1
|
||||||
|
@ -179,11 +195,19 @@ class TestImage:
|
||||||
def test_fp_name(self, tmp_path: Path) -> None:
|
def test_fp_name(self, tmp_path: Path) -> None:
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
temp_file = str(tmp_path / "temp.jpg")
|
||||||
|
|
||||||
class FP:
|
class FP(io.BytesIO):
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
def write(self, b: bytes) -> None:
|
if sys.version_info >= (3, 12):
|
||||||
pass
|
from collections.abc import Buffer
|
||||||
|
|
||||||
|
def write(self, data: Buffer) -> int:
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def write(self, data: Any) -> int:
|
||||||
|
return len(data)
|
||||||
|
|
||||||
fp = FP()
|
fp = FP()
|
||||||
fp.name = temp_file
|
fp.name = temp_file
|
||||||
|
@ -352,8 +376,9 @@ class TestImage:
|
||||||
img = Image.alpha_composite(dst, src)
|
img = Image.alpha_composite(dst, src)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
img_colors = sorted(img.getcolors())
|
img_colors = img.getcolors()
|
||||||
assert img_colors == expected_colors
|
assert img_colors is not None
|
||||||
|
assert sorted(img_colors) == expected_colors
|
||||||
|
|
||||||
def test_alpha_inplace(self) -> None:
|
def test_alpha_inplace(self) -> None:
|
||||||
src = Image.new("RGBA", (128, 128), "blue")
|
src = Image.new("RGBA", (128, 128), "blue")
|
||||||
|
@ -397,13 +422,13 @@ class TestImage:
|
||||||
|
|
||||||
# errors
|
# errors
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, "invalid source")
|
source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, (0, 0), "invalid destination")
|
source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, 0)
|
source.alpha_composite(over, 0) # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, (0, 0), 0)
|
source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
source.alpha_composite(over, (0, 0), (0, -1))
|
source.alpha_composite(over, (0, 0), (0, -1))
|
||||||
|
|
||||||
|
@ -555,6 +580,7 @@ class TestImage:
|
||||||
for mode in ("I", "F", "L"):
|
for mode in ("I", "F", "L"):
|
||||||
im = Image.new(mode, (100, 100), (5,))
|
im = Image.new(mode, (100, 100), (5,))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
assert px[0, 0] == 5
|
assert px[0, 0] == 5
|
||||||
|
|
||||||
def test_linear_gradient_wrong_mode(self) -> None:
|
def test_linear_gradient_wrong_mode(self) -> None:
|
||||||
|
@ -649,7 +675,9 @@ class TestImage:
|
||||||
|
|
||||||
im_remapped = im.remap_palette([1, 0])
|
im_remapped = im.remap_palette([1, 0])
|
||||||
assert im_remapped.info["transparency"] == 1
|
assert im_remapped.info["transparency"] == 1
|
||||||
assert len(im_remapped.getpalette()) == 6
|
palette = im_remapped.getpalette()
|
||||||
|
assert palette is not None
|
||||||
|
assert len(palette) == 6
|
||||||
|
|
||||||
# Test unused transparency
|
# Test unused transparency
|
||||||
im.info["transparency"] = 2
|
im.info["transparency"] = 2
|
||||||
|
@ -680,7 +708,7 @@ class TestImage:
|
||||||
else:
|
else:
|
||||||
assert new_image.palette is None
|
assert new_image.palette is None
|
||||||
|
|
||||||
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
|
_make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
|
||||||
_make_new(im_p, im, None)
|
_make_new(im_p, im, None)
|
||||||
_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())
|
||||||
|
@ -913,6 +941,25 @@ class TestImage:
|
||||||
assert tag not in exif.get_ifd(0x8769)
|
assert tag not in exif.get_ifd(0x8769)
|
||||||
assert exif.get_ifd(0xA005)
|
assert exif.get_ifd(0xA005)
|
||||||
|
|
||||||
|
def test_empty_xmp(self) -> None:
|
||||||
|
with Image.open("Tests/images/hopper.gif") as im:
|
||||||
|
assert im.getxmp() == {}
|
||||||
|
|
||||||
|
def test_getxmp_padded(self) -> None:
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
im.info["xmp"] = (
|
||||||
|
b'<?xpacket begin="\xef\xbb\xbf" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
|
||||||
|
b'<x:xmpmeta xmlns:x="adobe:ns:meta/" />\n<?xpacket end="w"?>\x00\x00'
|
||||||
|
)
|
||||||
|
if ElementTree is None:
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
|
assert im.getxmp() == {}
|
||||||
|
else:
|
||||||
|
assert im.getxmp() == {"xmpmeta": None}
|
||||||
|
|
||||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
|
|
|
@ -12,19 +12,6 @@ from PIL import Image
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper, is_win32
|
from .helper import assert_image_equal, hopper, is_win32
|
||||||
|
|
||||||
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
|
|
||||||
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
|
|
||||||
cffi: ModuleType | None
|
|
||||||
if os.environ.get("PYTHONOPTIMIZE") == "2":
|
|
||||||
cffi = None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
import cffi
|
|
||||||
|
|
||||||
from PIL import PyAccess
|
|
||||||
except ImportError:
|
|
||||||
cffi = None
|
|
||||||
|
|
||||||
numpy: ModuleType | None
|
numpy: ModuleType | None
|
||||||
try:
|
try:
|
||||||
import numpy
|
import numpy
|
||||||
|
@ -32,21 +19,7 @@ except ImportError:
|
||||||
numpy = None
|
numpy = None
|
||||||
|
|
||||||
|
|
||||||
class AccessTest:
|
class TestImagePutPixel:
|
||||||
# Initial value
|
|
||||||
_init_cffi_access = Image.USE_CFFI_ACCESS
|
|
||||||
_need_cffi_access = False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setup_class(cls) -> None:
|
|
||||||
Image.USE_CFFI_ACCESS = cls._need_cffi_access
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def teardown_class(cls) -> None:
|
|
||||||
Image.USE_CFFI_ACCESS = cls._init_cffi_access
|
|
||||||
|
|
||||||
|
|
||||||
class TestImagePutPixel(AccessTest):
|
|
||||||
def test_sanity(self) -> None:
|
def test_sanity(self) -> None:
|
||||||
im1 = hopper()
|
im1 = hopper()
|
||||||
im2 = Image.new(im1.mode, im1.size, 0)
|
im2 = Image.new(im1.mode, im1.size, 0)
|
||||||
|
@ -54,7 +27,9 @@ class TestImagePutPixel(AccessTest):
|
||||||
for y in range(im1.size[1]):
|
for y in range(im1.size[1]):
|
||||||
for x in range(im1.size[0]):
|
for x in range(im1.size[0]):
|
||||||
pos = x, y
|
pos = x, y
|
||||||
im2.putpixel(pos, im1.getpixel(pos))
|
value = im1.getpixel(pos)
|
||||||
|
assert value is not None
|
||||||
|
im2.putpixel(pos, value)
|
||||||
|
|
||||||
assert_image_equal(im1, im2)
|
assert_image_equal(im1, im2)
|
||||||
|
|
||||||
|
@ -64,7 +39,9 @@ class TestImagePutPixel(AccessTest):
|
||||||
for y in range(im1.size[1]):
|
for y in range(im1.size[1]):
|
||||||
for x in range(im1.size[0]):
|
for x in range(im1.size[0]):
|
||||||
pos = x, y
|
pos = x, y
|
||||||
im2.putpixel(pos, im1.getpixel(pos))
|
value = im1.getpixel(pos)
|
||||||
|
assert value is not None
|
||||||
|
im2.putpixel(pos, value)
|
||||||
|
|
||||||
assert not im2.readonly
|
assert not im2.readonly
|
||||||
assert_image_equal(im1, im2)
|
assert_image_equal(im1, im2)
|
||||||
|
@ -74,10 +51,12 @@ class TestImagePutPixel(AccessTest):
|
||||||
pix1 = im1.load()
|
pix1 = im1.load()
|
||||||
pix2 = im2.load()
|
pix2 = im2.load()
|
||||||
|
|
||||||
|
assert pix1 is not None
|
||||||
|
assert pix2 is not None
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
pix1[0, "0"]
|
pix1[0, "0"] # type: ignore[index]
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
pix1["0", 0]
|
pix1["0", 0] # type: ignore[index]
|
||||||
|
|
||||||
for y in range(im1.size[1]):
|
for y in range(im1.size[1]):
|
||||||
for x in range(im1.size[0]):
|
for x in range(im1.size[0]):
|
||||||
|
@ -96,7 +75,9 @@ class TestImagePutPixel(AccessTest):
|
||||||
for y in range(-1, -im1.size[1] - 1, -1):
|
for y in range(-1, -im1.size[1] - 1, -1):
|
||||||
for x in range(-1, -im1.size[0] - 1, -1):
|
for x in range(-1, -im1.size[0] - 1, -1):
|
||||||
pos = x, y
|
pos = x, y
|
||||||
im2.putpixel(pos, im1.getpixel(pos))
|
value = im1.getpixel(pos)
|
||||||
|
assert value is not None
|
||||||
|
im2.putpixel(pos, value)
|
||||||
|
|
||||||
assert_image_equal(im1, im2)
|
assert_image_equal(im1, im2)
|
||||||
|
|
||||||
|
@ -106,7 +87,9 @@ class TestImagePutPixel(AccessTest):
|
||||||
for y in range(-1, -im1.size[1] - 1, -1):
|
for y in range(-1, -im1.size[1] - 1, -1):
|
||||||
for x in range(-1, -im1.size[0] - 1, -1):
|
for x in range(-1, -im1.size[0] - 1, -1):
|
||||||
pos = x, y
|
pos = x, y
|
||||||
im2.putpixel(pos, im1.getpixel(pos))
|
value = im1.getpixel(pos)
|
||||||
|
assert value is not None
|
||||||
|
im2.putpixel(pos, value)
|
||||||
|
|
||||||
assert not im2.readonly
|
assert not im2.readonly
|
||||||
assert_image_equal(im1, im2)
|
assert_image_equal(im1, im2)
|
||||||
|
@ -116,6 +99,8 @@ class TestImagePutPixel(AccessTest):
|
||||||
pix1 = im1.load()
|
pix1 = im1.load()
|
||||||
pix2 = im2.load()
|
pix2 = im2.load()
|
||||||
|
|
||||||
|
assert pix1 is not None
|
||||||
|
assert pix2 is not None
|
||||||
for y in range(-1, -im1.size[1] - 1, -1):
|
for y in range(-1, -im1.size[1] - 1, -1):
|
||||||
for x in range(-1, -im1.size[0] - 1, -1):
|
for x in range(-1, -im1.size[0] - 1, -1):
|
||||||
pix2[x, y] = pix1[x, y]
|
pix2[x, y] = pix1[x, y]
|
||||||
|
@ -125,13 +110,14 @@ class TestImagePutPixel(AccessTest):
|
||||||
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
||||||
def test_numpy(self) -> None:
|
def test_numpy(self) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
pix = im.load()
|
px = im.load()
|
||||||
|
|
||||||
|
assert px is not None
|
||||||
assert numpy is not None
|
assert numpy is not None
|
||||||
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
|
assert px[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
|
||||||
|
|
||||||
|
|
||||||
class TestImageGetPixel(AccessTest):
|
class TestImageGetPixel:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def color(mode: str) -> int | tuple[int, ...]:
|
def color(mode: str) -> int | tuple[int, ...]:
|
||||||
bands = Image.getmodebands(mode)
|
bands = Image.getmodebands(mode)
|
||||||
|
@ -144,9 +130,6 @@ class TestImageGetPixel(AccessTest):
|
||||||
return tuple(range(1, bands + 1))
|
return tuple(range(1, bands + 1))
|
||||||
|
|
||||||
def check(self, mode: str, expected_color_int: int | None = None) -> None:
|
def check(self, mode: str, expected_color_int: int | None = None) -> None:
|
||||||
if self._need_cffi_access and mode.startswith("BGR;"):
|
|
||||||
pytest.skip("Support not added to deprecated module for BGR;* modes")
|
|
||||||
|
|
||||||
expected_color = (
|
expected_color = (
|
||||||
self.color(mode) if expected_color_int is None else expected_color_int
|
self.color(mode) if expected_color_int is None else expected_color_int
|
||||||
)
|
)
|
||||||
|
@ -171,15 +154,14 @@ class TestImageGetPixel(AccessTest):
|
||||||
# Check 0x0 image with None initial color
|
# Check 0x0 image with None initial color
|
||||||
im = Image.new(mode, (0, 0), None)
|
im = Image.new(mode, (0, 0), None)
|
||||||
assert im.load() is not None
|
assert im.load() is not None
|
||||||
error = ValueError if self._need_cffi_access else IndexError
|
with pytest.raises(IndexError):
|
||||||
with pytest.raises(error):
|
|
||||||
im.putpixel((0, 0), expected_color)
|
im.putpixel((0, 0), expected_color)
|
||||||
with pytest.raises(error):
|
with pytest.raises(IndexError):
|
||||||
im.getpixel((0, 0))
|
im.getpixel((0, 0))
|
||||||
# Check negative index
|
# Check negative index
|
||||||
with pytest.raises(error):
|
with pytest.raises(IndexError):
|
||||||
im.putpixel((-1, -1), expected_color)
|
im.putpixel((-1, -1), expected_color)
|
||||||
with pytest.raises(error):
|
with pytest.raises(IndexError):
|
||||||
im.getpixel((-1, -1))
|
im.getpixel((-1, -1))
|
||||||
|
|
||||||
# Check initial color
|
# Check initial color
|
||||||
|
@ -199,10 +181,10 @@ class TestImageGetPixel(AccessTest):
|
||||||
|
|
||||||
# Check 0x0 image with initial color
|
# Check 0x0 image with initial color
|
||||||
im = Image.new(mode, (0, 0), expected_color)
|
im = Image.new(mode, (0, 0), expected_color)
|
||||||
with pytest.raises(error):
|
with pytest.raises(IndexError):
|
||||||
im.getpixel((0, 0))
|
im.getpixel((0, 0))
|
||||||
# Check negative index
|
# Check negative index
|
||||||
with pytest.raises(error):
|
with pytest.raises(IndexError):
|
||||||
im.getpixel((-1, -1))
|
im.getpixel((-1, -1))
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", Image.MODES)
|
@pytest.mark.parametrize("mode", Image.MODES)
|
||||||
|
@ -235,126 +217,7 @@ class TestImageGetPixel(AccessTest):
|
||||||
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
|
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
class TestImagePutPixelError:
|
||||||
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
|
||||||
class TestCffiPutPixel(TestImagePutPixel):
|
|
||||||
_need_cffi_access = True
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
|
||||||
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
|
||||||
class TestCffiGetPixel(TestImageGetPixel):
|
|
||||||
_need_cffi_access = True
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
|
||||||
class TestCffi(AccessTest):
|
|
||||||
_need_cffi_access = True
|
|
||||||
|
|
||||||
def _test_get_access(self, im: Image.Image) -> None:
|
|
||||||
"""Do we get the same thing as the old pixel access
|
|
||||||
|
|
||||||
Using private interfaces, forcing a capi access and
|
|
||||||
a pyaccess for the same image"""
|
|
||||||
caccess = im.im.pixel_access(False)
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
access = PyAccess.new(im, False)
|
|
||||||
assert access is not None
|
|
||||||
|
|
||||||
w, h = im.size
|
|
||||||
for x in range(0, w, 10):
|
|
||||||
for y in range(0, h, 10):
|
|
||||||
assert access[(x, y)] == caccess[(x, y)]
|
|
||||||
|
|
||||||
# Access an out-of-range pixel
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
access[(access.xsize + 1, access.ysize + 1)]
|
|
||||||
|
|
||||||
def test_get_vs_c(self) -> None:
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
rgb = hopper("RGB")
|
|
||||||
rgb.load()
|
|
||||||
self._test_get_access(rgb)
|
|
||||||
for mode in ("RGBA", "L", "LA", "1", "P", "F"):
|
|
||||||
self._test_get_access(hopper(mode))
|
|
||||||
|
|
||||||
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
|
|
||||||
im = Image.new(mode, (10, 10), 40000)
|
|
||||||
self._test_get_access(im)
|
|
||||||
|
|
||||||
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
|
|
||||||
"""Are we writing the correct bits into the image?
|
|
||||||
|
|
||||||
Using private interfaces, forcing a capi access and
|
|
||||||
a pyaccess for the same image"""
|
|
||||||
caccess = im.im.pixel_access(False)
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
access = PyAccess.new(im, False)
|
|
||||||
assert access is not None
|
|
||||||
|
|
||||||
w, h = im.size
|
|
||||||
for x in range(0, w, 10):
|
|
||||||
for y in range(0, h, 10):
|
|
||||||
access[(x, y)] = color
|
|
||||||
assert color == caccess[(x, y)]
|
|
||||||
|
|
||||||
# Attempt to set the value on a read-only image
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
access = PyAccess.new(im, True)
|
|
||||||
assert access is not None
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
access[(0, 0)] = color
|
|
||||||
|
|
||||||
def test_set_vs_c(self) -> None:
|
|
||||||
rgb = hopper("RGB")
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
rgb.load()
|
|
||||||
self._test_set_access(rgb, (255, 128, 0))
|
|
||||||
self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
|
|
||||||
self._test_set_access(hopper("L"), 128)
|
|
||||||
self._test_set_access(hopper("LA"), (128, 128))
|
|
||||||
self._test_set_access(hopper("1"), 255)
|
|
||||||
self._test_set_access(hopper("P"), 128)
|
|
||||||
self._test_set_access(hopper("PA"), (128, 128))
|
|
||||||
self._test_set_access(hopper("F"), 1024.0)
|
|
||||||
|
|
||||||
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
|
|
||||||
im = Image.new(mode, (10, 10), 40000)
|
|
||||||
self._test_set_access(im, 45000)
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
|
||||||
def test_not_implemented(self) -> None:
|
|
||||||
assert PyAccess.new(hopper("BGR;15")) is None
|
|
||||||
|
|
||||||
# Ref https://github.com/python-pillow/Pillow/pull/2009
|
|
||||||
def test_reference_counting(self) -> None:
|
|
||||||
size = 10
|
|
||||||
|
|
||||||
for _ in range(10):
|
|
||||||
# Do not save references to the image, only to the access object
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
px = Image.new("L", (size, 1), 0).load()
|
|
||||||
for i in range(size):
|
|
||||||
# Pixels can contain garbage if image is released
|
|
||||||
assert px[i, 0] == 0
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
|
||||||
def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
|
|
||||||
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
|
|
||||||
im = Image.new(mode, (1, 1))
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
access = PyAccess.new(im, False)
|
|
||||||
assert access is not None
|
|
||||||
|
|
||||||
access.putpixel((0, 0), color)
|
|
||||||
|
|
||||||
if len(color) == 3:
|
|
||||||
color += (255,)
|
|
||||||
assert im.convert("RGBA").getpixel((0, 0)) == color
|
|
||||||
|
|
||||||
|
|
||||||
class TestImagePutPixelError(AccessTest):
|
|
||||||
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
|
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
|
||||||
IMAGE_MODES2 = ["L", "I", "I;16"]
|
IMAGE_MODES2 = ["L", "I", "I;16"]
|
||||||
INVALID_TYPES = ["foo", 1.0, None]
|
INVALID_TYPES = ["foo", 1.0, None]
|
||||||
|
@ -364,7 +227,7 @@ class TestImagePutPixelError(AccessTest):
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
for v in self.INVALID_TYPES:
|
for v in self.INVALID_TYPES:
|
||||||
with pytest.raises(TypeError, match="color must be int or tuple"):
|
with pytest.raises(TypeError, match="color must be int or tuple"):
|
||||||
im.putpixel((0, 0), v)
|
im.putpixel((0, 0), v) # type: ignore[arg-type]
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("mode", "band_numbers", "match"),
|
("mode", "band_numbers", "match"),
|
||||||
|
@ -398,7 +261,7 @@ class TestImagePutPixelError(AccessTest):
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
TypeError, match="color must be int or single-element tuple"
|
TypeError, match="color must be int or single-element tuple"
|
||||||
):
|
):
|
||||||
im.putpixel((0, 0), v)
|
im.putpixel((0, 0), v) # type: ignore[arg-type]
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
|
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
|
||||||
def test_putpixel_overflow_error(self, mode: str) -> None:
|
def test_putpixel_overflow_error(self, mode: str) -> None:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
@ -13,13 +13,16 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
||||||
|
|
||||||
im = hopper().resize((128, 100))
|
im = hopper().resize((128, 100))
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
|
||||||
def test_toarray() -> None:
|
def test_toarray() -> None:
|
||||||
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
|
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
|
||||||
ai = numpy.array(im.convert(mode))
|
ai = numpy.array(im.convert(mode))
|
||||||
return ai.shape, ai.dtype.str, ai.nbytes
|
return ai.shape, ai.dtype.str, ai.nbytes
|
||||||
|
|
||||||
def test_with_dtype(dtype) -> None:
|
def test_with_dtype(dtype: npt.DTypeLike) -> None:
|
||||||
ai = numpy.array(im, dtype=dtype)
|
ai = numpy.array(im, dtype=dtype)
|
||||||
assert ai.dtype == dtype
|
assert ai.dtype == dtype
|
||||||
|
|
||||||
|
|
|
@ -222,8 +222,10 @@ def test_l_macro_rounding(convert_mode: str) -> None:
|
||||||
|
|
||||||
converted_im = im.convert(convert_mode)
|
converted_im = im.convert(convert_mode)
|
||||||
px = converted_im.load()
|
px = converted_im.load()
|
||||||
|
assert px is not None
|
||||||
converted_color = px[0, 0]
|
converted_color = px[0, 0]
|
||||||
if convert_mode == "LA":
|
if convert_mode == "LA":
|
||||||
|
assert isinstance(converted_color, tuple)
|
||||||
converted_color = converted_color[0]
|
converted_color = converted_color[0]
|
||||||
assert converted_color == 1
|
assert converted_color == 1
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@ def draft_roundtrip(
|
||||||
im = Image.new(in_mode, in_size)
|
im = Image.new(in_mode, in_size)
|
||||||
data = tostring(im, "JPEG")
|
data = tostring(im, "JPEG")
|
||||||
im = fromstring(data)
|
im = fromstring(data)
|
||||||
mode, box = im.draft(req_mode, req_size)
|
result = im.draft(req_mode, req_size)
|
||||||
|
assert result is not None
|
||||||
|
box = result[1]
|
||||||
scale, _ = im.decoderconfig
|
scale, _ = im.decoderconfig
|
||||||
assert box[:2] == (0, 0)
|
assert box[:2] == (0, 0)
|
||||||
assert (im.width - scale) < box[2] <= im.width
|
assert (im.width - scale) < box[2] <= im.width
|
||||||
|
|
|
@ -137,7 +137,7 @@ def test_builtinfilter_p() -> None:
|
||||||
builtin_filter = ImageFilter.BuiltinFilter()
|
builtin_filter = ImageFilter.BuiltinFilter()
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
builtin_filter.filter(hopper("P"))
|
builtin_filter.filter(hopper("P").im)
|
||||||
|
|
||||||
|
|
||||||
def test_kernel_not_enough_coefficients() -> None:
|
def test_kernel_not_enough_coefficients() -> None:
|
||||||
|
|
|
@ -54,17 +54,21 @@ def test_pack() -> None:
|
||||||
assert A is None
|
assert A is None
|
||||||
|
|
||||||
A = im.getcolors(maxcolors=3)
|
A = im.getcolors(maxcolors=3)
|
||||||
|
assert A is not None
|
||||||
A.sort()
|
A.sort()
|
||||||
assert A == expected
|
assert A == expected
|
||||||
|
|
||||||
A = im.getcolors(maxcolors=4)
|
A = im.getcolors(maxcolors=4)
|
||||||
|
assert A is not None
|
||||||
A.sort()
|
A.sort()
|
||||||
assert A == expected
|
assert A == expected
|
||||||
|
|
||||||
A = im.getcolors(maxcolors=8)
|
A = im.getcolors(maxcolors=8)
|
||||||
|
assert A is not None
|
||||||
A.sort()
|
A.sort()
|
||||||
assert A == expected
|
assert A == expected
|
||||||
|
|
||||||
A = im.getcolors(maxcolors=16)
|
A = im.getcolors(maxcolors=16)
|
||||||
|
assert A is not None
|
||||||
A.sort()
|
A.sort()
|
||||||
assert A == expected
|
assert A == expected
|
||||||
|
|
|
@ -12,9 +12,10 @@ from .helper import hopper
|
||||||
|
|
||||||
def test_sanity() -> None:
|
def test_sanity() -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
pix = im.load()
|
px = im.load()
|
||||||
|
|
||||||
assert pix[0, 0] == (20, 20, 70)
|
assert px is not None
|
||||||
|
assert px[0, 0] == (20, 20, 70)
|
||||||
|
|
||||||
|
|
||||||
def test_close() -> None:
|
def test_close() -> None:
|
||||||
|
|
|
@ -14,6 +14,7 @@ class TestImagingPaste:
|
||||||
self, im: Image.Image, expected: list[tuple[int, int, int, int]]
|
self, im: Image.Image, expected: list[tuple[int, int, int, int]]
|
||||||
) -> None:
|
) -> None:
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
actual = [
|
actual = [
|
||||||
px[0, 0],
|
px[0, 0],
|
||||||
px[self.size // 2, 0],
|
px[self.size // 2, 0],
|
||||||
|
@ -48,6 +49,7 @@ class TestImagingPaste:
|
||||||
def mask_1(self) -> Image.Image:
|
def mask_1(self) -> Image.Image:
|
||||||
mask = Image.new("1", (self.size, self.size))
|
mask = Image.new("1", (self.size, self.size))
|
||||||
px = mask.load()
|
px = mask.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(mask.height):
|
for y in range(mask.height):
|
||||||
for x in range(mask.width):
|
for x in range(mask.width):
|
||||||
px[y, x] = (x + y) % 2
|
px[y, x] = (x + y) % 2
|
||||||
|
@ -61,6 +63,7 @@ class TestImagingPaste:
|
||||||
def gradient_L(self) -> Image.Image:
|
def gradient_L(self) -> Image.Image:
|
||||||
gradient = Image.new("L", (self.size, self.size))
|
gradient = Image.new("L", (self.size, self.size))
|
||||||
px = gradient.load()
|
px = gradient.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(gradient.height):
|
for y in range(gradient.height):
|
||||||
for x in range(gradient.width):
|
for x in range(gradient.width):
|
||||||
px[y, x] = (x + y) % 255
|
px[y, x] = (x + y) % 255
|
||||||
|
@ -338,3 +341,8 @@ class TestImagingPaste:
|
||||||
|
|
||||||
im.copy().paste(im2)
|
im.copy().paste(im2)
|
||||||
im.copy().paste(im2, (0, 0))
|
im.copy().paste(im2, (0, 0))
|
||||||
|
|
||||||
|
def test_incorrect_abbreviated_form(self) -> None:
|
||||||
|
im = Image.new("L", (1, 1))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.paste(im, im, im)
|
||||||
|
|
|
@ -61,4 +61,4 @@ def test_f_lut() -> None:
|
||||||
def test_f_mode() -> None:
|
def test_f_mode() -> None:
|
||||||
im = hopper("F")
|
im = hopper("F")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.point(None)
|
im.point([])
|
||||||
|
|
|
@ -31,7 +31,7 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
def test_long_integers() -> None:
|
def test_long_integers() -> None:
|
||||||
# see bug-200802-systemerror
|
# see bug-200802-systemerror
|
||||||
def put(value: int) -> tuple[int, int, int, int]:
|
def put(value: int) -> float | tuple[int, ...] | None:
|
||||||
im = Image.new("RGBA", (1, 1))
|
im = Image.new("RGBA", (1, 1))
|
||||||
im.putdata([value])
|
im.putdata([value])
|
||||||
return im.getpixel((0, 0))
|
return im.getpixel((0, 0))
|
||||||
|
@ -113,13 +113,13 @@ def test_array_F() -> None:
|
||||||
def test_not_flattened() -> None:
|
def test_not_flattened() -> None:
|
||||||
im = Image.new("L", (1, 1))
|
im = Image.new("L", (1, 1))
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
im.putdata([[0]]) # type: ignore[list-item]
|
im.putdata([[0]])
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
im.putdata([[0]], 2) # type: ignore[list-item]
|
im.putdata([[0]], 2)
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
im = Image.new("I", (1, 1))
|
im = Image.new("I", (1, 1))
|
||||||
im.putdata([[0]]) # type: ignore[list-item]
|
im.putdata([[0]])
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
im = Image.new("F", (1, 1))
|
im = Image.new("F", (1, 1))
|
||||||
im.putdata([[0]]) # type: ignore[list-item]
|
im.putdata([[0]])
|
||||||
|
|
|
@ -79,6 +79,7 @@ def test_putpalette_with_alpha_values() -> None:
|
||||||
(
|
(
|
||||||
("RGBA", (1, 2, 3, 4)),
|
("RGBA", (1, 2, 3, 4)),
|
||||||
("RGBAX", (1, 2, 3, 4, 0)),
|
("RGBAX", (1, 2, 3, 4, 0)),
|
||||||
|
("ARGB", (4, 1, 2, 3)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
|
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
|
||||||
|
|
|
@ -31,7 +31,9 @@ def test_libimagequant_quantize() -> None:
|
||||||
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
|
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
|
||||||
assert converted.mode == "P"
|
assert converted.mode == "P"
|
||||||
assert_image_similar(converted.convert("RGB"), image, 15)
|
assert_image_similar(converted.convert("RGB"), image, 15)
|
||||||
assert len(converted.getcolors()) == 100
|
colors = converted.getcolors()
|
||||||
|
assert colors is not None
|
||||||
|
assert len(colors) == 100
|
||||||
|
|
||||||
|
|
||||||
def test_octree_quantize() -> None:
|
def test_octree_quantize() -> None:
|
||||||
|
@ -39,7 +41,9 @@ def test_octree_quantize() -> None:
|
||||||
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
|
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
|
||||||
assert converted.mode == "P"
|
assert converted.mode == "P"
|
||||||
assert_image_similar(converted.convert("RGB"), image, 20)
|
assert_image_similar(converted.convert("RGB"), image, 20)
|
||||||
assert len(converted.getcolors()) == 100
|
colors = converted.getcolors()
|
||||||
|
assert colors is not None
|
||||||
|
assert len(colors) == 100
|
||||||
|
|
||||||
|
|
||||||
def test_rgba_quantize() -> None:
|
def test_rgba_quantize() -> None:
|
||||||
|
@ -80,6 +84,7 @@ def test_quantize_no_dither2() -> None:
|
||||||
assert tuple(quantized.palette.palette) == data
|
assert tuple(quantized.palette.palette) == data
|
||||||
|
|
||||||
px = quantized.load()
|
px = quantized.load()
|
||||||
|
assert px is not None
|
||||||
for x in range(9):
|
for x in range(9):
|
||||||
assert px[x, 0] == (0 if x < 5 else 1)
|
assert px[x, 0] == (0 if x < 5 else 1)
|
||||||
|
|
||||||
|
@ -118,10 +123,12 @@ def test_colors() -> None:
|
||||||
def test_transparent_colors_equal() -> None:
|
def test_transparent_colors_equal() -> None:
|
||||||
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
|
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
|
||||||
px = im.load()
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
px[0, 1] = (255, 255, 255, 0)
|
px[0, 1] = (255, 255, 255, 0)
|
||||||
|
|
||||||
converted = im.quantize()
|
converted = im.quantize()
|
||||||
converted_px = converted.load()
|
converted_px = converted.load()
|
||||||
|
assert converted_px is not None
|
||||||
assert converted_px[0, 0] == converted_px[0, 1]
|
assert converted_px[0, 0] == converted_px[0, 1]
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,6 +146,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
|
||||||
|
|
||||||
converted = im.quantize(method=method)
|
converted = im.quantize(method=method)
|
||||||
converted_px = converted.load()
|
converted_px = converted.load()
|
||||||
|
assert converted_px is not None
|
||||||
assert converted_px[0, 0] == converted.palette.colors[color]
|
assert converted_px[0, 0] == converted.palette.colors[color]
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,4 +162,6 @@ def test_small_palette() -> None:
|
||||||
im = im.quantize(palette=p)
|
im = im.quantize(palette=p)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert len(im.getcolors()) == 2
|
quantized_colors = im.getcolors()
|
||||||
|
assert quantized_colors is not None
|
||||||
|
assert len(quantized_colors) == 2
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -74,6 +74,7 @@ class TestImagingCoreResampleAccuracy:
|
||||||
data = data.replace(" ", "")
|
data = data.replace(" ", "")
|
||||||
sample = Image.new("L", size)
|
sample = Image.new("L", size)
|
||||||
s_px = sample.load()
|
s_px = sample.load()
|
||||||
|
assert s_px is not None
|
||||||
w, h = size[0] // 2, size[1] // 2
|
w, h = size[0] // 2, size[1] // 2
|
||||||
for y in range(h):
|
for y in range(h):
|
||||||
for x in range(w):
|
for x in range(w):
|
||||||
|
@ -87,6 +88,8 @@ class TestImagingCoreResampleAccuracy:
|
||||||
def check_case(self, case: Image.Image, sample: Image.Image) -> None:
|
def check_case(self, case: Image.Image, sample: Image.Image) -> None:
|
||||||
s_px = sample.load()
|
s_px = sample.load()
|
||||||
c_px = case.load()
|
c_px = case.load()
|
||||||
|
assert s_px is not None
|
||||||
|
assert c_px is not None
|
||||||
for y in range(case.size[1]):
|
for y in range(case.size[1]):
|
||||||
for x in range(case.size[0]):
|
for x in range(case.size[0]):
|
||||||
if c_px[x, y] != s_px[x, y]:
|
if c_px[x, y] != s_px[x, y]:
|
||||||
|
@ -98,6 +101,7 @@ class TestImagingCoreResampleAccuracy:
|
||||||
|
|
||||||
def serialize_image(self, image: Image.Image) -> str:
|
def serialize_image(self, image: Image.Image) -> str:
|
||||||
s_px = image.load()
|
s_px = image.load()
|
||||||
|
assert s_px is not None
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
|
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
|
||||||
for y in range(image.size[1])
|
for y in range(image.size[1])
|
||||||
|
@ -233,13 +237,16 @@ class TestImagingCoreResampleAccuracy:
|
||||||
class TestCoreResampleConsistency:
|
class TestCoreResampleConsistency:
|
||||||
def make_case(
|
def make_case(
|
||||||
self, mode: str, fill: tuple[int, int, int] | float
|
self, mode: str, fill: tuple[int, int, int] | float
|
||||||
) -> tuple[Image.Image, tuple[int, ...]]:
|
) -> tuple[Image.Image, float | tuple[int, ...]]:
|
||||||
im = Image.new(mode, (512, 9), fill)
|
im = Image.new(mode, (512, 9), fill)
|
||||||
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
|
px = im.load()
|
||||||
|
assert px is not None
|
||||||
|
return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
|
||||||
|
|
||||||
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
|
def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None:
|
||||||
channel, color = case
|
channel, color = case
|
||||||
px = channel.load()
|
px = channel.load()
|
||||||
|
assert px is not None
|
||||||
for x in range(channel.size[0]):
|
for x in range(channel.size[0]):
|
||||||
for y in range(channel.size[1]):
|
for y in range(channel.size[1]):
|
||||||
if px[x, y] != color:
|
if px[x, y] != color:
|
||||||
|
@ -249,6 +256,7 @@ class TestCoreResampleConsistency:
|
||||||
def test_8u(self) -> None:
|
def test_8u(self) -> None:
|
||||||
im, color = self.make_case("RGB", (0, 64, 255))
|
im, color = self.make_case("RGB", (0, 64, 255))
|
||||||
r, g, b = im.split()
|
r, g, b = im.split()
|
||||||
|
assert isinstance(color, tuple)
|
||||||
self.run_case((r, color[0]))
|
self.run_case((r, color[0]))
|
||||||
self.run_case((g, color[1]))
|
self.run_case((g, color[1]))
|
||||||
self.run_case((b, color[2]))
|
self.run_case((b, color[2]))
|
||||||
|
@ -271,6 +279,7 @@ class TestCoreResampleAlphaCorrect:
|
||||||
def make_levels_case(self, mode: str) -> Image.Image:
|
def make_levels_case(self, mode: str) -> Image.Image:
|
||||||
i = Image.new(mode, (256, 16))
|
i = Image.new(mode, (256, 16))
|
||||||
px = i.load()
|
px = i.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(i.size[1]):
|
for y in range(i.size[1]):
|
||||||
for x in range(i.size[0]):
|
for x in range(i.size[0]):
|
||||||
pix = [x] * len(mode)
|
pix = [x] * len(mode)
|
||||||
|
@ -280,8 +289,13 @@ class TestCoreResampleAlphaCorrect:
|
||||||
|
|
||||||
def run_levels_case(self, i: Image.Image) -> None:
|
def run_levels_case(self, i: Image.Image) -> None:
|
||||||
px = i.load()
|
px = i.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(i.size[1]):
|
for y in range(i.size[1]):
|
||||||
used_colors = {px[x, y][0] for x in range(i.size[0])}
|
used_colors = set()
|
||||||
|
for x in range(i.size[0]):
|
||||||
|
value = px[x, y]
|
||||||
|
assert isinstance(value, tuple)
|
||||||
|
used_colors.add(value[0])
|
||||||
assert 256 == len(used_colors), (
|
assert 256 == len(used_colors), (
|
||||||
"All colors should be present in resized image. "
|
"All colors should be present in resized image. "
|
||||||
f"Only {len(used_colors)} on line {y}."
|
f"Only {len(used_colors)} on line {y}."
|
||||||
|
@ -310,6 +324,7 @@ class TestCoreResampleAlphaCorrect:
|
||||||
) -> Image.Image:
|
) -> Image.Image:
|
||||||
i = Image.new(mode, (64, 64), dirty_pixel)
|
i = Image.new(mode, (64, 64), dirty_pixel)
|
||||||
px = i.load()
|
px = i.load()
|
||||||
|
assert px is not None
|
||||||
xdiv4 = i.size[0] // 4
|
xdiv4 = i.size[0] // 4
|
||||||
ydiv4 = i.size[1] // 4
|
ydiv4 = i.size[1] // 4
|
||||||
for y in range(ydiv4 * 2):
|
for y in range(ydiv4 * 2):
|
||||||
|
@ -319,14 +334,16 @@ class TestCoreResampleAlphaCorrect:
|
||||||
|
|
||||||
def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
|
def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
|
||||||
px = i.load()
|
px = i.load()
|
||||||
|
assert px is not None
|
||||||
for y in range(i.size[1]):
|
for y in range(i.size[1]):
|
||||||
for x in range(i.size[0]):
|
for x in range(i.size[0]):
|
||||||
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel:
|
value = px[x, y]
|
||||||
|
assert isinstance(value, tuple)
|
||||||
|
if value[-1] != 0 and value[:-1] != clean_pixel:
|
||||||
message = (
|
message = (
|
||||||
f"pixel at ({x}, {y}) is different:\n"
|
f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}"
|
||||||
f"{px[x, y]}\n{clean_pixel}"
|
|
||||||
)
|
)
|
||||||
assert px[x, y][:3] == clean_pixel, message
|
assert value[:3] == clean_pixel, message
|
||||||
|
|
||||||
def test_dirty_pixels_rgba(self) -> None:
|
def test_dirty_pixels_rgba(self) -> None:
|
||||||
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
|
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
|
||||||
|
@ -406,6 +423,7 @@ class TestCoreResampleCoefficients:
|
||||||
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
|
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
|
||||||
|
|
||||||
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
|
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
|
||||||
|
assert px is not None
|
||||||
if px[2, 0] != test_color // 2:
|
if px[2, 0] != test_color // 2:
|
||||||
assert test_color // 2 == px[2, 0]
|
assert test_color // 2 == px[2, 0]
|
||||||
|
|
||||||
|
@ -445,7 +463,7 @@ class TestCoreResampleBox:
|
||||||
im.resize((32, 32), resample, (20, 20, 100, 20))
|
im.resize((32, 32), resample, (20, 20, 100, 20))
|
||||||
|
|
||||||
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
||||||
im.resize((32, 32), resample, (im.width, im.height))
|
im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="can't be negative"):
|
with pytest.raises(ValueError, match="can't be negative"):
|
||||||
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
||||||
|
|
|
@ -4,9 +4,9 @@ Tests for resize functionality.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
from itertools import permutations
|
from itertools import permutations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -285,14 +285,14 @@ class TestReducingGapResize:
|
||||||
|
|
||||||
class TestImageResize:
|
class TestImageResize:
|
||||||
def test_resize(self) -> None:
|
def test_resize(self) -> None:
|
||||||
def resize(mode: str, size: tuple[int, int]) -> None:
|
def resize(mode: str, size: tuple[int, int] | list[int]) -> None:
|
||||||
out = hopper(mode).resize(size)
|
out = hopper(mode).resize(size)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == size
|
assert out.size == tuple(size)
|
||||||
|
|
||||||
for mode in "1", "P", "L", "RGB", "I", "F":
|
for mode in "1", "P", "L", "RGB", "I", "F":
|
||||||
resize(mode, (112, 103))
|
resize(mode, (112, 103))
|
||||||
resize(mode, (188, 214))
|
resize(mode, [188, 214])
|
||||||
|
|
||||||
# Test unknown resampling filter
|
# Test unknown resampling filter
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
|
|
|
@ -192,8 +192,9 @@ class TestImageTransform:
|
||||||
|
|
||||||
im = op(im, (40, 10))
|
im = op(im, (40, 10))
|
||||||
|
|
||||||
colors = sorted(im.getcolors())
|
colors = im.getcolors()
|
||||||
assert colors == sorted(
|
assert colors is not None
|
||||||
|
assert sorted(colors) == sorted(
|
||||||
(
|
(
|
||||||
(20 * 10, opaque),
|
(20 * 10, opaque),
|
||||||
(20 * 10, transparent),
|
(20 * 10, transparent),
|
||||||
|
|
|
@ -391,23 +391,25 @@ def test_overlay() -> None:
|
||||||
def test_logical() -> None:
|
def test_logical() -> None:
|
||||||
def table(
|
def table(
|
||||||
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
|
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
|
||||||
) -> tuple[int, int, int, int]:
|
) -> list[float]:
|
||||||
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)
|
||||||
for y in (a, b):
|
for y in (a, b):
|
||||||
imy = Image.new("1", (1, 1), y)
|
imy = Image.new("1", (1, 1), y)
|
||||||
out.append(op(imx, imy).getpixel((0, 0)))
|
value = op(imx, imy).getpixel((0, 0))
|
||||||
return tuple(out)
|
assert not isinstance(value, tuple) and value is not None
|
||||||
|
out.append(value)
|
||||||
|
return out
|
||||||
|
|
||||||
assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255)
|
assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255]
|
||||||
assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255)
|
assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255]
|
||||||
assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0)
|
assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0]
|
||||||
|
|
||||||
assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255)
|
assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255]
|
||||||
assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255)
|
assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255]
|
||||||
assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0)
|
assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0]
|
||||||
|
|
||||||
assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255)
|
assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255]
|
||||||
assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255)
|
assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255]
|
||||||
assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0)
|
assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0]
|
||||||
|
|
|
@ -103,7 +103,7 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_flags() -> None:
|
def test_flags() -> None:
|
||||||
assert ImageCms.Flags.NONE == 0
|
assert ImageCms.Flags.NONE.value == 0
|
||||||
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
|
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
|
||||||
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
|
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
|
||||||
|
|
||||||
|
@ -569,9 +569,9 @@ def assert_aux_channel_preserved(
|
||||||
for delta in nine_grid_deltas:
|
for delta in nine_grid_deltas:
|
||||||
channel_data.paste(
|
channel_data.paste(
|
||||||
channel_pattern,
|
channel_pattern,
|
||||||
tuple(
|
(
|
||||||
paste_offset[c] + delta[c] * channel_pattern.size[c]
|
paste_offset[0] + delta[0] * channel_pattern.size[0],
|
||||||
for c in range(2)
|
paste_offset[1] + delta[1] * channel_pattern.size[1],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
chans.append(channel_data)
|
chans.append(channel_data)
|
||||||
|
@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
|
||||||
# convert with and without AUX data, test colors are equal
|
# convert with and without AUX data, test colors are equal
|
||||||
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
|
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
|
||||||
source_profile = ImageCms.createProfile(src_colorSpace)
|
source_profile = ImageCms.createProfile(src_colorSpace)
|
||||||
destination_profile = ImageCms.createProfile(dst_format[1])
|
dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
|
||||||
|
destination_profile = ImageCms.createProfile(dst_colorSpace)
|
||||||
source_image = src_format[3]
|
source_image = src_format[3]
|
||||||
test_transform = ImageCms.buildTransform(
|
test_transform = ImageCms.buildTransform(
|
||||||
source_profile,
|
source_profile,
|
||||||
|
@ -678,7 +679,8 @@ def test_auxiliary_channels_isolated() -> None:
|
||||||
|
|
||||||
def test_long_modes() -> None:
|
def test_long_modes() -> None:
|
||||||
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
|
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
|
||||||
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
|
with pytest.warns(DeprecationWarning):
|
||||||
|
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
||||||
|
@ -689,7 +691,9 @@ def test_rgb_lab(mode: str) -> None:
|
||||||
|
|
||||||
im = Image.new("LAB", (1, 1), (255, 0, 0))
|
im = Image.new("LAB", (1, 1), (255, 0, 0))
|
||||||
converted_im = im.convert(mode)
|
converted_im = im.convert(mode)
|
||||||
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
|
value = converted_im.getpixel((0, 0))
|
||||||
|
assert isinstance(value, tuple)
|
||||||
|
assert value[:3] == (0, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_deprecation() -> None:
|
def test_deprecation() -> None:
|
||||||
|
@ -699,3 +703,9 @@ def test_deprecation() -> None:
|
||||||
assert ImageCms.VERSION == "1.0.0 pil"
|
assert ImageCms.VERSION == "1.0.0 pil"
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
assert isinstance(ImageCms.FLAGS, dict)
|
assert isinstance(ImageCms.FLAGS, dict)
|
||||||
|
|
||||||
|
profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
|
ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB")
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
|
ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B")
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import os.path
|
import os.path
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -448,6 +448,7 @@ def test_shape1() -> None:
|
||||||
x3, y3 = 95, 5
|
x3, y3 = 95, 5
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.move(x0, y0)
|
s.move(x0, y0)
|
||||||
s.curve(x1, y1, x2, y2, x3, y3)
|
s.curve(x1, y1, x2, y2, x3, y3)
|
||||||
|
@ -469,6 +470,7 @@ def test_shape2() -> None:
|
||||||
x3, y3 = 5, 95
|
x3, y3 = 5, 95
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.move(x0, y0)
|
s.move(x0, y0)
|
||||||
s.curve(x1, y1, x2, y2, x3, y3)
|
s.curve(x1, y1, x2, y2, x3, y3)
|
||||||
|
@ -487,6 +489,7 @@ def test_transform() -> None:
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.line(0, 0)
|
s.line(0, 0)
|
||||||
s.transform((0, 0, 0, 0, 0, 0))
|
s.transform((0, 0, 0, 0, 0, 0))
|
||||||
|
@ -631,6 +634,19 @@ def test_polygon(points: Coords) -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
|
def test_polygon_width_I16(points: Coords) -> None:
|
||||||
|
# Arrange
|
||||||
|
im = Image.new("I;16", (W, H))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
draw.polygon(points, outline=0xFFFF, width=2)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon_width_I.tiff")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
||||||
@pytest.mark.parametrize("kite_points", KITE_POINTS)
|
@pytest.mark.parametrize("kite_points", KITE_POINTS)
|
||||||
def test_polygon_kite(
|
def test_polygon_kite(
|
||||||
|
@ -913,7 +929,12 @@ def test_rounded_rectangle_translucent(
|
||||||
def test_floodfill(bbox: Coords) -> None:
|
def test_floodfill(bbox: Coords) -> None:
|
||||||
red = ImageColor.getrgb("red")
|
red = ImageColor.getrgb("red")
|
||||||
|
|
||||||
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
|
mode_values: list[tuple[str, int | tuple[int, ...]]] = [
|
||||||
|
("L", 1),
|
||||||
|
("RGBA", (255, 0, 0, 0)),
|
||||||
|
("RGB", red),
|
||||||
|
]
|
||||||
|
for mode, value in mode_values:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new(mode, (W, H))
|
im = Image.new(mode, (W, H))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
@ -1401,25 +1422,44 @@ def test_default_font_size() -> None:
|
||||||
|
|
||||||
im = Image.new("RGB", (220, 25))
|
im = Image.new("RGB", (220, 25))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
|
||||||
|
def check(func: Callable[[], None]) -> None:
|
||||||
|
if freetype_support:
|
||||||
|
func()
|
||||||
|
else:
|
||||||
|
with pytest.raises(ImportError):
|
||||||
|
func()
|
||||||
|
|
||||||
|
def draw_text() -> None:
|
||||||
draw.text((0, 0), text, font_size=16)
|
draw.text((0, 0), text, font_size=16)
|
||||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
||||||
|
|
||||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
check(draw_text)
|
||||||
|
|
||||||
|
def draw_textlength() -> None:
|
||||||
assert draw.textlength(text, font_size=16) == 216
|
assert draw.textlength(text, font_size=16) == 216
|
||||||
|
|
||||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
check(draw_textlength)
|
||||||
|
|
||||||
|
def draw_textbbox() -> None:
|
||||||
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
|
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
|
||||||
|
|
||||||
|
check(draw_textbbox)
|
||||||
|
|
||||||
im = Image.new("RGB", (220, 25))
|
im = Image.new("RGB", (220, 25))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
|
||||||
|
def draw_multiline_text() -> None:
|
||||||
draw.multiline_text((0, 0), text, font_size=16)
|
draw.multiline_text((0, 0), text, font_size=16)
|
||||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
||||||
|
|
||||||
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
|
check(draw_multiline_text)
|
||||||
|
|
||||||
|
def draw_multiline_textbbox() -> None:
|
||||||
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
|
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
|
||||||
|
|
||||||
|
check(draw_multiline_textbbox)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
def test_same_color_outline(bbox: Coords) -> None:
|
def test_same_color_outline(bbox: Coords) -> None:
|
||||||
|
@ -1429,6 +1469,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
||||||
x2, y2 = 95, 50
|
x2, y2 = 95, 50
|
||||||
x3, y3 = 95, 5
|
x3, y3 = 95, 5
|
||||||
|
|
||||||
|
assert ImageDraw.Outline is not None
|
||||||
s = ImageDraw.Outline()
|
s = ImageDraw.Outline()
|
||||||
s.move(x0, y0)
|
s.move(x0, y0)
|
||||||
s.curve(x1, y1, x2, y2, x3, y3)
|
s.curve(x1, y1, x2, y2, x3, y3)
|
||||||
|
@ -1467,7 +1508,7 @@ def test_same_color_outline(bbox: Coords) -> None:
|
||||||
(4, "square", {}),
|
(4, "square", {}),
|
||||||
(8, "regular_octagon", {}),
|
(8, "regular_octagon", {}),
|
||||||
(4, "square_rotate_45", {"rotation": 45}),
|
(4, "square_rotate_45", {"rotation": 45}),
|
||||||
(3, "triangle_width", {"width": 5, "outline": "yellow"}),
|
(3, "triangle_width", {"outline": "yellow", "width": 5}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_draw_regular_polygon(
|
def test_draw_regular_polygon(
|
||||||
|
@ -1477,7 +1518,10 @@ def test_draw_regular_polygon(
|
||||||
filename = f"Tests/images/imagedraw_{polygon_name}.png"
|
filename = f"Tests/images/imagedraw_{polygon_name}.png"
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
bounding_circle = ((W // 2, H // 2), 25)
|
bounding_circle = ((W // 2, H // 2), 25)
|
||||||
draw.regular_polygon(bounding_circle, n_sides, fill="red", **args)
|
rotation = int(args.get("rotation", 0))
|
||||||
|
outline = args.get("outline")
|
||||||
|
width = int(args.get("width", 1))
|
||||||
|
draw.regular_polygon(bounding_circle, n_sides, rotation, "red", outline, width)
|
||||||
assert_image_equal_tofile(im, filename)
|
assert_image_equal_tofile(im, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1569,7 +1613,7 @@ def test_compute_regular_polygon_vertices_input_error_handling(
|
||||||
error_message: str,
|
error_message: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
with pytest.raises(expected_error) as e:
|
with pytest.raises(expected_error) as e:
|
||||||
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
|
||||||
assert str(e.value) == error_message
|
assert str(e.value) == error_message
|
||||||
|
|
||||||
|
|
||||||
|
@ -1630,6 +1674,6 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
|
||||||
draw.rounded_rectangle(xy)
|
draw.rounded_rectangle(xy)
|
||||||
|
|
||||||
|
|
||||||
def test_getdraw():
|
def test_getdraw() -> None:
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
ImageDraw.getdraw(None, [])
|
ImageDraw.getdraw(None, [])
|
||||||
|
|
|
@ -51,9 +51,18 @@ def test_sanity() -> None:
|
||||||
pen = ImageDraw2.Pen("blue", width=7)
|
pen = ImageDraw2.Pen("blue", width=7)
|
||||||
draw.line(list(range(10)), pen)
|
draw.line(list(range(10)), pen)
|
||||||
|
|
||||||
draw, handler = ImageDraw.getdraw(im)
|
draw2, handler = ImageDraw.getdraw(im)
|
||||||
|
assert draw2 is not None
|
||||||
pen = ImageDraw2.Pen("blue", width=7)
|
pen = ImageDraw2.Pen("blue", width=7)
|
||||||
draw.line(list(range(10)), pen)
|
draw2.line(list(range(10)), pen)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mode() -> None:
|
||||||
|
draw = ImageDraw2.Draw("L", (1, 1))
|
||||||
|
assert draw.image.mode == "L"
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageDraw2.Draw("L")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
|
|
|
@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
|
||||||
|
|
||||||
super().__init__(mode, *args)
|
super().__init__(mode, *args)
|
||||||
|
|
||||||
def decode(self, buffer):
|
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||||
# eof
|
# eof
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
|
@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
|
||||||
|
|
||||||
super().__init__(mode, *args)
|
super().__init__(mode, *args)
|
||||||
|
|
||||||
def encode(self, buffer):
|
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
|
||||||
return 1, 1, b""
|
return 1, 1, b""
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
|
@ -305,7 +305,7 @@ class TestPyDecoder(CodecsTest):
|
||||||
def test_decode(self) -> None:
|
def test_decode(self) -> None:
|
||||||
decoder = ImageFile.PyDecoder(None)
|
decoder = ImageFile.PyDecoder(None)
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
decoder.decode(None)
|
decoder.decode(b"")
|
||||||
|
|
||||||
|
|
||||||
class TestPyEncoder(CodecsTest):
|
class TestPyEncoder(CodecsTest):
|
||||||
|
@ -351,7 +351,9 @@ class TestPyEncoder(CodecsTest):
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||||
)
|
)
|
||||||
assert MockPyEncoder.last.cleanup_called
|
last: MockPyEncoder | None = MockPyEncoder.last
|
||||||
|
assert last
|
||||||
|
assert last.cleanup_called
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
|
@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
|
||||||
def test_encode(self) -> None:
|
def test_encode(self) -> None:
|
||||||
encoder = ImageFile.PyEncoder(None)
|
encoder = ImageFile.PyEncoder(None)
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
encoder.encode(None)
|
encoder.encode(0)
|
||||||
|
|
||||||
bytes_consumed, errcode = encoder.encode_to_pyfd()
|
bytes_consumed, errcode = encoder.encode_to_pyfd()
|
||||||
assert bytes_consumed == 0
|
assert bytes_consumed == 0
|
||||||
|
|
|
@ -209,7 +209,7 @@ def test_getlength(
|
||||||
assert length == length_raqm
|
assert length == length_raqm
|
||||||
|
|
||||||
|
|
||||||
def test_float_size() -> None:
|
def test_float_size(layout_engine: ImageFont.Layout) -> None:
|
||||||
lengths = []
|
lengths = []
|
||||||
for size in (48, 48.5, 49):
|
for size in (48, 48.5, 49):
|
||||||
f = ImageFont.truetype(
|
f = ImageFont.truetype(
|
||||||
|
@ -494,8 +494,8 @@ def test_default_font() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
|
||||||
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
|
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
|
||||||
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
||||||
|
|
||||||
|
|
||||||
|
@ -548,7 +548,7 @@ def test_find_font(
|
||||||
|
|
||||||
def loadable_font(
|
def loadable_font(
|
||||||
filepath: str, size: int, index: int, encoding: str, *args: Any
|
filepath: str, size: int, index: int, encoding: str, *args: Any
|
||||||
):
|
) -> ImageFont.FreeTypeFont:
|
||||||
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
|
_freeTypeFont = getattr(ImageFont, "_FreeTypeFont")
|
||||||
if filepath == path_to_fake:
|
if filepath == path_to_fake:
|
||||||
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
|
return _freeTypeFont(FONT_PATH, size, index, encoding, *args)
|
||||||
|
@ -564,6 +564,7 @@ def test_find_font(
|
||||||
# catching syntax like errors
|
# catching syntax like errors
|
||||||
monkeypatch.setattr(sys, "platform", platform)
|
monkeypatch.setattr(sys, "platform", platform)
|
||||||
if platform == "linux":
|
if platform == "linux":
|
||||||
|
monkeypatch.setenv("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
||||||
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
||||||
|
|
||||||
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
|
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
|
||||||
|
@ -1096,6 +1097,23 @@ def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
|
||||||
imagefont.getmask("A" * 1_000_001)
|
imagefont.getmask("A" * 1_000_001)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bytes(font: ImageFont.FreeTypeFont) -> None:
|
||||||
|
assert font.getlength(b"test") == font.getlength("test")
|
||||||
|
|
||||||
|
assert font.getbbox(b"test") == font.getbbox("test")
|
||||||
|
|
||||||
|
assert_image_equal(
|
||||||
|
Image.Image()._new(font.getmask(b"test")),
|
||||||
|
Image.Image()._new(font.getmask("test")),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_image_equal(
|
||||||
|
Image.Image()._new(font.getmask2(b"test")[0]),
|
||||||
|
Image.Image()._new(font.getmask2("test")[0]),
|
||||||
|
)
|
||||||
|
assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file",
|
"test_file",
|
||||||
[
|
[
|
||||||
|
|
|
@ -9,51 +9,57 @@ from PIL import Image, ImageDraw, ImageFont, _util, features
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile
|
from .helper import assert_image_equal_tofile
|
||||||
|
|
||||||
original_core = ImageFont.core
|
fonts = [ImageFont.load_default_imagefont()]
|
||||||
|
if not features.check_module("freetype2"):
|
||||||
|
default_font = ImageFont.load_default()
|
||||||
|
if isinstance(default_font, ImageFont.ImageFont):
|
||||||
|
fonts.append(default_font)
|
||||||
|
|
||||||
|
|
||||||
def setup_module() -> None:
|
@pytest.mark.parametrize("font", fonts)
|
||||||
if features.check_module("freetype2"):
|
def test_default_font(font: ImageFont.ImageFont) -> None:
|
||||||
ImageFont.core = _util.DeferredError(ImportError)
|
|
||||||
|
|
||||||
|
|
||||||
def teardown_module() -> None:
|
|
||||||
ImageFont.core = original_core
|
|
||||||
|
|
||||||
|
|
||||||
def test_default_font() -> None:
|
|
||||||
# Arrange
|
# Arrange
|
||||||
txt = 'This is a "better than nothing" default font.'
|
txt = 'This is a "better than nothing" default font.'
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
default_font = ImageFont.load_default()
|
draw.text((10, 10), txt, font=font)
|
||||||
draw.text((10, 10), txt, font=default_font)
|
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert_image_equal_tofile(im, "Tests/images/default_font.png")
|
assert_image_equal_tofile(im, "Tests/images/default_font.png")
|
||||||
|
|
||||||
|
|
||||||
def test_size_without_freetype() -> None:
|
def test_without_freetype() -> None:
|
||||||
with pytest.raises(ImportError):
|
original_core = ImageFont.core
|
||||||
ImageFont.load_default(size=14)
|
if features.check_module("freetype2"):
|
||||||
|
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
|
||||||
|
try:
|
||||||
|
with pytest.raises(ImportError):
|
||||||
|
ImageFont.truetype("Tests/fonts/FreeMono.ttf")
|
||||||
|
|
||||||
|
assert isinstance(ImageFont.load_default(), ImageFont.ImageFont)
|
||||||
|
|
||||||
|
with pytest.raises(ImportError):
|
||||||
|
ImageFont.load_default(size=14)
|
||||||
|
finally:
|
||||||
|
ImageFont.core = original_core
|
||||||
|
|
||||||
|
|
||||||
def test_unicode() -> None:
|
@pytest.mark.parametrize("font", fonts)
|
||||||
|
def test_unicode(font: ImageFont.ImageFont) -> None:
|
||||||
# should not segfault, should return UnicodeDecodeError
|
# should not segfault, should return UnicodeDecodeError
|
||||||
# issue #2826
|
# issue #2826
|
||||||
font = ImageFont.load_default()
|
|
||||||
with pytest.raises(UnicodeEncodeError):
|
with pytest.raises(UnicodeEncodeError):
|
||||||
font.getbbox("’")
|
font.getbbox("’")
|
||||||
|
|
||||||
|
|
||||||
def test_textbbox() -> None:
|
@pytest.mark.parametrize("font", fonts)
|
||||||
|
def test_textbbox(font: ImageFont.ImageFont) -> None:
|
||||||
im = Image.new("RGB", (200, 200))
|
im = Image.new("RGB", (200, 200))
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
default_font = ImageFont.load_default()
|
assert d.textlength("test", font=font) == 24
|
||||||
assert d.textlength("test", font=default_font) == 24
|
assert d.textbbox((0, 0), "test", font=font) == (0, 0, 24, 11)
|
||||||
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
|
|
||||||
|
|
||||||
|
|
||||||
def test_decompression_bomb() -> None:
|
def test_decompression_bomb() -> None:
|
||||||
|
|
|
@ -60,6 +60,8 @@ class TestImageGrab:
|
||||||
def test_grabclipboard(self) -> None:
|
def test_grabclipboard(self) -> None:
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
subprocess.call(["screencapture", "-cx"])
|
subprocess.call(["screencapture", "-cx"])
|
||||||
|
|
||||||
|
ImageGrab.grabclipboard()
|
||||||
elif sys.platform == "win32":
|
elif sys.platform == "win32":
|
||||||
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
||||||
p.stdin.write(
|
p.stdin.write(
|
||||||
|
@ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
[Windows.Forms.Clipboard]::SetImage($bmp)"""
|
[Windows.Forms.Clipboard]::SetImage($bmp)"""
|
||||||
)
|
)
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
|
ImageGrab.grabclipboard()
|
||||||
else:
|
else:
|
||||||
if not shutil.which("wl-paste") and not shutil.which("xclip"):
|
if not shutil.which("wl-paste") and not shutil.which("xclip"):
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
|
@ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
r" ImageGrab.grabclipboard\(\) on Linux",
|
r" ImageGrab.grabclipboard\(\) on Linux",
|
||||||
):
|
):
|
||||||
ImageGrab.grabclipboard()
|
ImageGrab.grabclipboard()
|
||||||
return
|
|
||||||
|
|
||||||
ImageGrab.grabclipboard()
|
|
||||||
|
|
||||||
@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:
|
||||||
|
@ -89,6 +90,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
im = ImageGrab.grabclipboard()
|
im = ImageGrab.grabclipboard()
|
||||||
|
assert isinstance(im, list)
|
||||||
assert len(im) == 1
|
assert len(im) == 1
|
||||||
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
|
assert os.path.samefile(im[0], "Tests/images/hopper.gif")
|
||||||
|
|
||||||
|
@ -105,6 +107,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
im = ImageGrab.grabclipboard()
|
im = ImageGrab.grabclipboard()
|
||||||
|
assert isinstance(im, Image.Image)
|
||||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
@ -120,6 +123,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
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)
|
||||||
im = ImageGrab.grabclipboard()
|
im = ImageGrab.grabclipboard()
|
||||||
|
assert isinstance(im, Image.Image)
|
||||||
assert_image_equal_tofile(im, image_path)
|
assert_image_equal_tofile(im, image_path)
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
|
|
@ -41,11 +41,15 @@ A = string_to_img(
|
||||||
def img_to_string(im: Image.Image) -> str:
|
def img_to_string(im: Image.Image) -> str:
|
||||||
"""Turn a (small) binary image into a string representation"""
|
"""Turn a (small) binary image into a string representation"""
|
||||||
chars = ".1"
|
chars = ".1"
|
||||||
width, height = im.size
|
result = []
|
||||||
return "\n".join(
|
for r in range(im.height):
|
||||||
"".join(chars[im.getpixel((c, r)) > 0] for c in range(width))
|
line = ""
|
||||||
for r in range(height)
|
for c in range(im.width):
|
||||||
)
|
value = im.getpixel((c, r))
|
||||||
|
assert not isinstance(value, tuple) and value is not None
|
||||||
|
line += chars[value > 0]
|
||||||
|
result.append(line)
|
||||||
|
return "\n".join(result)
|
||||||
|
|
||||||
|
|
||||||
def img_string_normalize(im: str) -> str:
|
def img_string_normalize(im: str) -> str:
|
||||||
|
|
|
@ -165,10 +165,14 @@ def test_pad() -> None:
|
||||||
def test_pad_round() -> None:
|
def test_pad_round() -> None:
|
||||||
im = Image.new("1", (1, 1), 1)
|
im = Image.new("1", (1, 1), 1)
|
||||||
new_im = ImageOps.pad(im, (4, 1))
|
new_im = ImageOps.pad(im, (4, 1))
|
||||||
assert new_im.load()[2, 0] == 1
|
px = new_im.load()
|
||||||
|
assert px is not None
|
||||||
|
assert px[2, 0] == 1
|
||||||
|
|
||||||
new_im = ImageOps.pad(im, (1, 4))
|
new_im = ImageOps.pad(im, (1, 4))
|
||||||
assert new_im.load()[0, 2] == 1
|
px = new_im.load()
|
||||||
|
assert px is not None
|
||||||
|
assert px[0, 2] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
|
@ -223,6 +227,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
|
||||||
else:
|
else:
|
||||||
left, top, right, bottom = border
|
left, top, right, bottom = border
|
||||||
px = im_expanded.convert("RGB").load()
|
px = im_expanded.convert("RGB").load()
|
||||||
|
assert px is not None
|
||||||
for x in range(im_expanded.width):
|
for x in range(im_expanded.width):
|
||||||
for b in range(top):
|
for b in range(top):
|
||||||
assert px[x, b] == (255, 0, 0)
|
assert px[x, b] == (255, 0, 0)
|
||||||
|
@ -254,20 +259,26 @@ def test_colorize_2color() -> None:
|
||||||
left = (0, 1)
|
left = (0, 1)
|
||||||
middle = (127, 1)
|
middle = (127, 1)
|
||||||
right = (255, 1)
|
right = (255, 1)
|
||||||
|
value = im_test.getpixel(left)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(left),
|
value,
|
||||||
(255, 0, 0),
|
(255, 0, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="black test pixel incorrect",
|
msg="black test pixel incorrect",
|
||||||
)
|
)
|
||||||
|
value = im_test.getpixel(middle)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(middle),
|
value,
|
||||||
(127, 63, 0),
|
(127, 63, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="mid test pixel incorrect",
|
msg="mid test pixel incorrect",
|
||||||
)
|
)
|
||||||
|
value = im_test.getpixel(right)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(right),
|
value,
|
||||||
(0, 127, 0),
|
(0, 127, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="white test pixel incorrect",
|
msg="white test pixel incorrect",
|
||||||
|
@ -290,20 +301,26 @@ def test_colorize_2color_offset() -> None:
|
||||||
left = (25, 1)
|
left = (25, 1)
|
||||||
middle = (75, 1)
|
middle = (75, 1)
|
||||||
right = (125, 1)
|
right = (125, 1)
|
||||||
|
value = im_test.getpixel(left)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(left),
|
value,
|
||||||
(255, 0, 0),
|
(255, 0, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="black test pixel incorrect",
|
msg="black test pixel incorrect",
|
||||||
)
|
)
|
||||||
|
value = im_test.getpixel(middle)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(middle),
|
value,
|
||||||
(127, 63, 0),
|
(127, 63, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="mid test pixel incorrect",
|
msg="mid test pixel incorrect",
|
||||||
)
|
)
|
||||||
|
value = im_test.getpixel(right)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(right),
|
value,
|
||||||
(0, 127, 0),
|
(0, 127, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="white test pixel incorrect",
|
msg="white test pixel incorrect",
|
||||||
|
@ -334,29 +351,37 @@ def test_colorize_3color_offset() -> None:
|
||||||
middle = (100, 1)
|
middle = (100, 1)
|
||||||
right_middle = (150, 1)
|
right_middle = (150, 1)
|
||||||
right = (225, 1)
|
right = (225, 1)
|
||||||
|
value = im_test.getpixel(left)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(left),
|
value,
|
||||||
(255, 0, 0),
|
(255, 0, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="black test pixel incorrect",
|
msg="black test pixel incorrect",
|
||||||
)
|
)
|
||||||
|
value = im_test.getpixel(left_middle)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(left_middle),
|
value,
|
||||||
(127, 0, 127),
|
(127, 0, 127),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="low-mid test pixel incorrect",
|
msg="low-mid test pixel incorrect",
|
||||||
)
|
)
|
||||||
|
value = im_test.getpixel(middle)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
|
assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
|
||||||
|
value = im_test.getpixel(right_middle)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect"
|
value,
|
||||||
)
|
|
||||||
assert_tuple_approx_equal(
|
|
||||||
im_test.getpixel(right_middle),
|
|
||||||
(0, 63, 127),
|
(0, 63, 127),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="high-mid test pixel incorrect",
|
msg="high-mid test pixel incorrect",
|
||||||
)
|
)
|
||||||
|
value = im_test.getpixel(right)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
im_test.getpixel(right),
|
value,
|
||||||
(0, 127, 0),
|
(0, 127, 0),
|
||||||
threshold=1,
|
threshold=1,
|
||||||
msg="white test pixel incorrect",
|
msg="white test pixel incorrect",
|
||||||
|
@ -432,6 +457,17 @@ def test_exif_transpose() -> None:
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
|
||||||
|
def test_exif_transpose_xml_without_xmp() -> None:
|
||||||
|
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
||||||
|
assert im.getexif()[0x0112] == 3
|
||||||
|
assert "XML:com.adobe.xmp" in im.info
|
||||||
|
|
||||||
|
del im.info["xmp"]
|
||||||
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert transposed_im is not None
|
||||||
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
|
||||||
def test_exif_transpose_in_place() -> None:
|
def test_exif_transpose_in_place() -> None:
|
||||||
with Image.open("Tests/images/orientation_rectangle.jpg") as im:
|
with Image.open("Tests/images/orientation_rectangle.jpg") as im:
|
||||||
assert im.size == (2, 1)
|
assert im.size == (2, 1)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Generator
|
from collections.abc import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
|
||||||
assert i.im.getpixel((x, y))[c] >= 250
|
assert i.im.getpixel((x, y))[c] >= 250
|
||||||
# Fuzzy match.
|
# Fuzzy match.
|
||||||
|
|
||||||
def gp(x, y):
|
def gp(x: int, y: int) -> tuple[int, ...]:
|
||||||
return i.im.getpixel((x, y))
|
return i.im.getpixel((x, y))
|
||||||
|
|
||||||
assert 236 <= gp(7, 4)[0] <= 239
|
assert 236 <= gp(7, 4)[0] <= 239
|
||||||
|
|
|
@ -45,7 +45,7 @@ def test_getcolor() -> None:
|
||||||
|
|
||||||
# Test unknown color specifier
|
# Test unknown color specifier
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
palette.getcolor("unknown")
|
palette.getcolor("unknown") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
def test_getcolor_rgba_color_rgb_palette() -> None:
|
def test_getcolor_rgba_color_rgb_palette() -> None:
|
||||||
|
@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
|
||||||
|
|
||||||
palette.save(f)
|
palette.save(f)
|
||||||
|
|
||||||
p = ImagePalette.load(f)
|
lut = ImagePalette.load(f)
|
||||||
|
|
||||||
# load returns raw palette information
|
# load returns raw palette information
|
||||||
assert len(p[0]) == 768
|
assert len(lut[0]) == 768
|
||||||
assert p[1] == "RGB"
|
assert lut[1] == "RGB"
|
||||||
|
|
||||||
p = ImagePalette.raw(p[1], p[0])
|
p = ImagePalette.raw(lut[1], lut[0])
|
||||||
assert isinstance(p, ImagePalette.ImagePalette)
|
assert isinstance(p, ImagePalette.ImagePalette)
|
||||||
assert p.palette == palette.tobytes()
|
assert p.palette == palette.tobytes()
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
import array
|
import array
|
||||||
import math
|
import math
|
||||||
import struct
|
import struct
|
||||||
from typing import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
|
@ -41,18 +41,13 @@ def test_rgb() -> None:
|
||||||
checkrgb(0, 0, 255)
|
checkrgb(0, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_image() -> None:
|
@pytest.mark.parametrize("mode", ("1", "RGB", "RGBA", "L", "P", "I;16"))
|
||||||
modes = ["1", "RGB", "RGBA", "L", "P"]
|
def test_image(mode: str) -> None:
|
||||||
qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
|
im = hopper(mode)
|
||||||
if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
|
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
||||||
modes.append("I;16")
|
if mode not in ("RGB", "RGBA"):
|
||||||
|
im = im.convert("RGB")
|
||||||
for mode in modes:
|
assert_image_similar(roundtripped_im, im, 1)
|
||||||
im = hopper(mode)
|
|
||||||
roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
|
|
||||||
if mode not in ("RGB", "RGBA"):
|
|
||||||
im = im.convert("RGB")
|
|
||||||
assert_image_similar(roundtripped_im, im, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file() -> None:
|
def test_closed_file() -> None:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -15,8 +16,11 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_register() -> None:
|
def test_register() -> None:
|
||||||
# Test registering a viewer that is not a class
|
# Test registering a viewer that is an instance
|
||||||
ImageShow.register("not a class")
|
class TestViewer(ImageShow.Viewer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ImageShow.register(TestViewer())
|
||||||
|
|
||||||
# Restore original state
|
# Restore original state
|
||||||
ImageShow._viewers.pop()
|
ImageShow._viewers.pop()
|
||||||
|
@ -65,6 +69,27 @@ def test_show_without_viewers() -> None:
|
||||||
ImageShow._viewers = viewers
|
ImageShow._viewers = viewers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"viewer",
|
||||||
|
(
|
||||||
|
ImageShow.Viewer(),
|
||||||
|
ImageShow.WindowsViewer(),
|
||||||
|
ImageShow.MacViewer(),
|
||||||
|
ImageShow.XDGViewer(),
|
||||||
|
ImageShow.DisplayViewer(),
|
||||||
|
ImageShow.GmDisplayViewer(),
|
||||||
|
ImageShow.EogViewer(),
|
||||||
|
ImageShow.XVViewer(),
|
||||||
|
ImageShow.IPythonViewer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_show_file(viewer: ImageShow.Viewer) -> None:
|
||||||
|
assert not os.path.exists("missing.png")
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
viewer.show_file("missing.png")
|
||||||
|
|
||||||
|
|
||||||
def test_viewer() -> None:
|
def test_viewer() -> None:
|
||||||
viewer = ImageShow.Viewer()
|
viewer = ImageShow.Viewer()
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,12 @@ def test_kw() -> None:
|
||||||
|
|
||||||
# Test "file"
|
# Test "file"
|
||||||
im = ImageTk._get_image_from_kw(kw)
|
im = ImageTk._get_image_from_kw(kw)
|
||||||
|
assert im is not None
|
||||||
assert_image_equal(im, im1)
|
assert_image_equal(im, im1)
|
||||||
|
|
||||||
# Test "data"
|
# Test "data"
|
||||||
im = ImageTk._get_image_from_kw(kw)
|
im = ImageTk._get_image_from_kw(kw)
|
||||||
|
assert im is not None
|
||||||
assert_image_equal(im, im2)
|
assert_image_equal(im, im2)
|
||||||
|
|
||||||
# Test no relevant entry
|
# Test no relevant entry
|
||||||
|
@ -70,6 +72,11 @@ def test_photoimage(mode: str) -> None:
|
||||||
reloaded = ImageTk.getimage(im_tk)
|
reloaded = ImageTk.getimage(im_tk)
|
||||||
assert_image_equal(reloaded, im.convert("RGBA"))
|
assert_image_equal(reloaded, im.convert("RGBA"))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageTk.PhotoImage()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageTk.PhotoImage(mode)
|
||||||
|
|
||||||
|
|
||||||
def test_photoimage_apply_transparency() -> None:
|
def test_photoimage_apply_transparency() -> None:
|
||||||
with Image.open("Tests/images/pil123p.png") as im:
|
with Image.open("Tests/images/pil123p.png") as im:
|
||||||
|
|
|
@ -45,21 +45,22 @@ if is_win32():
|
||||||
memcpy = ctypes.cdll.msvcrt.memcpy
|
memcpy = ctypes.cdll.msvcrt.memcpy
|
||||||
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
|
memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
|
||||||
|
|
||||||
CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC
|
windll = getattr(ctypes, "windll")
|
||||||
|
CreateCompatibleDC = windll.gdi32.CreateCompatibleDC
|
||||||
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
|
CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC]
|
||||||
CreateCompatibleDC.restype = ctypes.wintypes.HDC
|
CreateCompatibleDC.restype = ctypes.wintypes.HDC
|
||||||
|
|
||||||
DeleteDC = ctypes.windll.gdi32.DeleteDC
|
DeleteDC = windll.gdi32.DeleteDC
|
||||||
DeleteDC.argtypes = [ctypes.wintypes.HDC]
|
DeleteDC.argtypes = [ctypes.wintypes.HDC]
|
||||||
|
|
||||||
SelectObject = ctypes.windll.gdi32.SelectObject
|
SelectObject = windll.gdi32.SelectObject
|
||||||
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
|
SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ]
|
||||||
SelectObject.restype = ctypes.wintypes.HGDIOBJ
|
SelectObject.restype = ctypes.wintypes.HGDIOBJ
|
||||||
|
|
||||||
DeleteObject = ctypes.windll.gdi32.DeleteObject
|
DeleteObject = windll.gdi32.DeleteObject
|
||||||
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
|
DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ]
|
||||||
|
|
||||||
CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection
|
CreateDIBSection = windll.gdi32.CreateDIBSection
|
||||||
CreateDIBSection.argtypes = [
|
CreateDIBSection.argtypes = [
|
||||||
ctypes.wintypes.HDC,
|
ctypes.wintypes.HDC,
|
||||||
ctypes.c_void_p,
|
ctypes.c_void_p,
|
||||||
|
|
|
@ -16,6 +16,8 @@ def verify(im1: Image.Image) -> None:
|
||||||
assert im1.size == im2.size
|
assert im1.size == im2.size
|
||||||
pix1 = im1.load()
|
pix1 = im1.load()
|
||||||
pix2 = im2.load()
|
pix2 = im2.load()
|
||||||
|
assert pix1 is not None
|
||||||
|
assert pix2 is not None
|
||||||
for y in range(im1.size[1]):
|
for y in range(im1.size[1]):
|
||||||
for x in range(im1.size[0]):
|
for x in range(im1.size[0]):
|
||||||
xy = x, y
|
xy = x, y
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, _typing
|
||||||
|
|
||||||
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
|
from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import numpy
|
import numpy
|
||||||
import numpy.typing
|
import numpy.typing as npt
|
||||||
else:
|
else:
|
||||||
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
numpy = pytest.importorskip("numpy", reason="NumPy not installed")
|
||||||
|
|
||||||
|
@ -19,9 +19,7 @@ TEST_IMAGE_SIZE = (10, 10)
|
||||||
|
|
||||||
|
|
||||||
def test_numpy_to_image() -> None:
|
def test_numpy_to_image() -> None:
|
||||||
def to_image(
|
def to_image(dtype: npt.DTypeLike, bands: int = 1, boolean: int = 0) -> Image.Image:
|
||||||
dtype: numpy.typing.DTypeLike, 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
|
||||||
|
@ -106,13 +104,12 @@ 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(
|
def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> None:
|
||||||
img: Image.Image, np_img: numpy.typing.NDArray[Any]
|
|
||||||
) -> None:
|
|
||||||
assert len(np_img.shape) >= 2
|
assert len(np_img.shape) >= 2
|
||||||
np_size = np_img.shape[1], np_img.shape[0]
|
np_size = np_img.shape[1], np_img.shape[0]
|
||||||
assert img.size == np_size
|
assert img.size == np_size
|
||||||
px = img.load()
|
px = img.load()
|
||||||
|
assert px is not None
|
||||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||||
assert_deep_equal(px[x, y], np_img[y, x])
|
assert_deep_equal(px[x, y], np_img[y, x])
|
||||||
|
@ -145,6 +142,7 @@ def test_save_tiff_uint16() -> None:
|
||||||
img = Image.fromarray(a)
|
img = Image.fromarray(a)
|
||||||
|
|
||||||
img_px = img.load()
|
img_px = img.load()
|
||||||
|
assert img_px is not None
|
||||||
assert img_px[0, 0] == pixel_value
|
assert img_px[0, 0] == pixel_value
|
||||||
|
|
||||||
|
|
||||||
|
@ -166,7 +164,7 @@ def test_save_tiff_uint16() -> None:
|
||||||
("HSV", numpy.uint8),
|
("HSV", numpy.uint8),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_to_array(mode: str, dtype: numpy.typing.DTypeLike) -> None:
|
def test_to_array(mode: str, dtype: npt.DTypeLike) -> None:
|
||||||
img = hopper(mode)
|
img = hopper(mode)
|
||||||
|
|
||||||
# Resize to non-square
|
# Resize to non-square
|
||||||
|
@ -200,6 +198,15 @@ def test_putdata() -> None:
|
||||||
assert len(im.getdata()) == len(arr)
|
assert len(im.getdata()) == len(arr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resize() -> None:
|
||||||
|
im = hopper()
|
||||||
|
size = (64, 64)
|
||||||
|
|
||||||
|
im_resized = im.resize(numpy.array(size))
|
||||||
|
|
||||||
|
assert im_resized.size == size
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"dtype",
|
"dtype",
|
||||||
(
|
(
|
||||||
|
@ -216,7 +223,7 @@ def test_putdata() -> None:
|
||||||
numpy.float64,
|
numpy.float64,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_roundtrip_eye(dtype: numpy.typing.DTypeLike) -> None:
|
def test_roundtrip_eye(dtype: npt.DTypeLike) -> None:
|
||||||
arr = numpy.eye(10, dtype=dtype)
|
arr = numpy.eye(10, dtype=dtype)
|
||||||
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
|
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))
|
||||||
|
|
||||||
|
|
|
@ -54,16 +54,12 @@ def test_stdout(buffer: bool) -> None:
|
||||||
# Temporarily redirect stdout
|
# Temporarily redirect stdout
|
||||||
old_stdout = sys.stdout
|
old_stdout = sys.stdout
|
||||||
|
|
||||||
if buffer:
|
class MyStdOut:
|
||||||
|
buffer = BytesIO()
|
||||||
|
|
||||||
class MyStdOut:
|
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||||
buffer = BytesIO()
|
|
||||||
|
|
||||||
mystdout = MyStdOut()
|
sys.stdout = mystdout # type: ignore[assignment]
|
||||||
else:
|
|
||||||
mystdout = BytesIO()
|
|
||||||
|
|
||||||
sys.stdout = mystdout
|
|
||||||
|
|
||||||
ps = PSDraw.PSDraw()
|
ps = PSDraw.PSDraw()
|
||||||
_create_document(ps)
|
_create_document(ps)
|
||||||
|
@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
|
||||||
# Reset stdout
|
# Reset stdout
|
||||||
sys.stdout = old_stdout
|
sys.stdout = old_stdout
|
||||||
|
|
||||||
if buffer:
|
if isinstance(mystdout, MyStdOut):
|
||||||
mystdout = mystdout.buffer
|
mystdout = mystdout.buffer
|
||||||
assert mystdout.getvalue() != b""
|
assert mystdout.getvalue() != b""
|
||||||
|
|
|
@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
|
||||||
from .helper import hopper, skip_unless_feature
|
from .helper import hopper, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
def _test_equal(num, denom, target) -> None:
|
def _test_equal(
|
||||||
|
num: float | Fraction | IFDRational,
|
||||||
|
denom: int,
|
||||||
|
target: float | Fraction | IFDRational,
|
||||||
|
) -> None:
|
||||||
t = IFDRational(num, denom)
|
t = IFDRational(num, denom)
|
||||||
|
|
||||||
assert target == t
|
assert target == t
|
||||||
|
|
|
@ -17,6 +17,5 @@ coverage:
|
||||||
# Matches 'omit:' in .coveragerc
|
# Matches 'omit:' in .coveragerc
|
||||||
ignore:
|
ignore:
|
||||||
- "Tests/32bit_segfault_check.py"
|
- "Tests/32bit_segfault_check.py"
|
||||||
- "Tests/bench_cffi_access.py"
|
|
||||||
- "Tests/check_*.py"
|
- "Tests/check_*.py"
|
||||||
- "Tests/createfontdatachunk.py"
|
- "Tests/createfontdatachunk.py"
|
||||||
|
|
|
@ -338,7 +338,7 @@ linkcheck_allowed_redirects = {
|
||||||
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
|
# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
|
||||||
_repo = "https://github.com/python-pillow/Pillow/"
|
_repo = "https://github.com/python-pillow/Pillow/"
|
||||||
extlinks = {
|
extlinks = {
|
||||||
"cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"),
|
"cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"),
|
||||||
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
|
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
|
||||||
"issue": (_repo + "issues/%s", "#%s"),
|
"issue": (_repo + "issues/%s", "#%s"),
|
||||||
"pr": (_repo + "pull/%s", "#%s"),
|
"pr": (_repo + "pull/%s", "#%s"),
|
||||||
|
|
|
@ -12,28 +12,6 @@ Deprecated features
|
||||||
Below are features which are considered deprecated. Where appropriate,
|
Below are features which are considered deprecated. Where appropriate,
|
||||||
a :py:exc:`DeprecationWarning` is issued.
|
a :py:exc:`DeprecationWarning` is issued.
|
||||||
|
|
||||||
PSFile
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
.. deprecated:: 9.5.0
|
|
||||||
|
|
||||||
The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will
|
|
||||||
be removed in Pillow 11 (2024-10-15). This class was only made as a helper to
|
|
||||||
be used internally, so there is no replacement. If you need this functionality
|
|
||||||
though, it is a very short class that can easily be recreated in your own code.
|
|
||||||
|
|
||||||
PyAccess and Image.USE_CFFI_ACCESS
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. deprecated:: 10.0.0
|
|
||||||
|
|
||||||
Since Pillow's C API is now faster than PyAccess on PyPy,
|
|
||||||
:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow
|
|
||||||
11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead.
|
|
||||||
|
|
||||||
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
|
|
||||||
similarly deprecated.
|
|
||||||
|
|
||||||
ImageFile.raise_oserror
|
ImageFile.raise_oserror
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -107,6 +85,15 @@ BGR;15, BGR 16 and BGR;24
|
||||||
|
|
||||||
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
|
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
|
||||||
|
|
||||||
|
Non-image modes in ImageCms
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. deprecated:: 10.4.0
|
||||||
|
|
||||||
|
The use in :py:mod:`.ImageCms` of input modes and output modes that are not Pillow
|
||||||
|
image modes has been deprecated. Defaulting to "L" or "1" if the mode cannot be mapped
|
||||||
|
is also deprecated.
|
||||||
|
|
||||||
Support for LibTIFF earlier than 4
|
Support for LibTIFF earlier than 4
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -128,6 +115,29 @@ Removed features
|
||||||
Deprecated features are only removed in major releases after an appropriate
|
Deprecated features are only removed in major releases after an appropriate
|
||||||
period of deprecation has passed.
|
period of deprecation has passed.
|
||||||
|
|
||||||
|
PSFile
|
||||||
|
~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 9.5.0
|
||||||
|
.. versionremoved:: 11.0.0
|
||||||
|
|
||||||
|
The :py:class:`!PSFile` class was removed in Pillow 11 (2024-10-15).
|
||||||
|
This class was only made as a helper to be used internally,
|
||||||
|
so there is no replacement. If you need this functionality though,
|
||||||
|
it is a very short class that can easily be recreated in your own code.
|
||||||
|
|
||||||
|
PyAccess and Image.USE_CFFI_ACCESS
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. deprecated:: 10.0.0
|
||||||
|
.. versionremoved:: 11.0.0
|
||||||
|
|
||||||
|
Since Pillow's C API is now faster than PyAccess on PyPy, ``PyAccess`` has been
|
||||||
|
removed. Pillow's C API will now be used on PyPy instead.
|
||||||
|
|
||||||
|
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
|
||||||
|
similarly removed.
|
||||||
|
|
||||||
Tk/Tcl 8.4
|
Tk/Tcl 8.4
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -286,7 +296,7 @@ Previously, the ``size`` methods returned a ``height`` that included the vertica
|
||||||
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
offset of the text, while the new ``bbox`` methods distinguish this as a ``top``
|
||||||
offset.
|
offset.
|
||||||
|
|
||||||
.. image:: ./example/size_vs_bbox.png
|
.. image:: ./example/size_vs_bbox.webp
|
||||||
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
:alt: In bbox methods, top measures the vertical distance above the text, while bottom measures that plus the vertical distance of the text itself. In size methods, height also measures the vertical distance above the text plus the vertical distance of the text itself.
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 9.5 KiB |
|
@ -26,5 +26,5 @@ if __name__ == "__main__":
|
||||||
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
|
d.line(((x * 200, y * 100), (x * 200, (y + 1) * 100)), "black", 3)
|
||||||
if y != 0:
|
if y != 0:
|
||||||
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
|
d.line(((x * 200, y * 100), ((x + 1) * 200, y * 100)), "black", 3)
|
||||||
im.save("docs/example/anchors.png")
|
im.save("docs/example/anchors.webp")
|
||||||
im.show()
|
im.show()
|
||||||
|
|
BIN
docs/example/anchors.webp
Normal file
After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/image_thumbnail.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_contain.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 38 KiB |
BIN
docs/example/imageops_cover.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
docs/example/imageops_fit.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 19 KiB |