Merge branch 'main' into progress
|
@ -6,6 +6,7 @@ init:
|
||||||
# Uncomment previous line to get RDP access during the build.
|
# Uncomment previous line to get RDP access during the build.
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
EXECUTABLE: python.exe
|
EXECUTABLE: python.exe
|
||||||
TEST_OPTIONS:
|
TEST_OPTIONS:
|
||||||
DEPLOY: YES
|
DEPLOY: YES
|
||||||
|
|
1
.ci/requirements-mypy.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mypy==1.9.0
|
|
@ -12,6 +12,9 @@ exclude_also =
|
||||||
except ImportError
|
except ImportError
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
# Empty bodies in protocols or abstract methods
|
||||||
|
^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$
|
||||||
|
^\s*\.\.\.(\s*#.*)?$
|
||||||
|
|
||||||
[run]
|
[run]
|
||||||
omit =
|
omit =
|
||||||
|
|
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
||||||
tidelift: "pypi/Pillow"
|
tidelift: "pypi/pillow"
|
||||||
|
|
2
.github/workflows/docs.yml
vendored
|
@ -7,10 +7,12 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "src/PIL/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "src/PIL/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
3
.github/workflows/test-cygwin.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
5
.github/workflows/test-mingw.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
@ -64,10 +67,10 @@ jobs:
|
||||||
mingw-w64-x86_64-python3-cffi \
|
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-pip \
|
|
||||||
mingw-w64-x86_64-python3-setuptools \
|
mingw-w64-x86_64-python3-setuptools \
|
||||||
mingw-w64-x86_64-python-pyqt6
|
mingw-w64-x86_64-python-pyqt6
|
||||||
|
|
||||||
|
python3 -m ensurepip
|
||||||
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
||||||
|
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
17
.github/workflows/test-windows.yml
vendored
|
@ -26,13 +26,16 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"]
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
@ -66,8 +69,16 @@ jobs:
|
||||||
- name: Print build system information
|
- name: Print build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
- name: Install Python dependencies
|
||||||
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
run: >
|
||||||
|
python3 -m pip install
|
||||||
|
coverage>=7.4.2
|
||||||
|
defusedxml
|
||||||
|
olefile
|
||||||
|
pyroma
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-timeout
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: install
|
id: install
|
||||||
|
|
1
.github/workflows/test.yml
vendored
|
@ -27,6 +27,7 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
41
.github/workflows/wheels-dependencies.sh
vendored
|
@ -17,30 +17,30 @@ ARCHIVE_SDIR=pillow-depends-main
|
||||||
# Package versions for fresh source builds
|
# Package versions for fresh source builds
|
||||||
FREETYPE_VERSION=2.13.2
|
FREETYPE_VERSION=2.13.2
|
||||||
HARFBUZZ_VERSION=8.3.0
|
HARFBUZZ_VERSION=8.3.0
|
||||||
LIBPNG_VERSION=1.6.40
|
LIBPNG_VERSION=1.6.43
|
||||||
JPEGTURBO_VERSION=3.0.1
|
JPEGTURBO_VERSION=3.0.2
|
||||||
OPENJPEG_VERSION=2.5.0
|
OPENJPEG_VERSION=2.5.2
|
||||||
XZ_VERSION=5.4.5
|
XZ_VERSION=5.4.5
|
||||||
TIFF_VERSION=4.6.0
|
TIFF_VERSION=4.6.0
|
||||||
LCMS2_VERSION=2.16
|
LCMS2_VERSION=2.16
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
GIFLIB_VERSION=5.1.4
|
GIFLIB_VERSION=5.2.2
|
||||||
else
|
else
|
||||||
GIFLIB_VERSION=5.2.1
|
GIFLIB_VERSION=5.2.1
|
||||||
fi
|
fi
|
||||||
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
||||||
ZLIB_VERSION=1.3
|
ZLIB_VERSION=1.3.1
|
||||||
else
|
else
|
||||||
ZLIB_VERSION=1.2.8
|
ZLIB_VERSION=1.2.8
|
||||||
fi
|
fi
|
||||||
LIBWEBP_VERSION=1.3.2
|
LIBWEBP_VERSION=1.3.2
|
||||||
BZIP2_VERSION=1.0.8
|
BZIP2_VERSION=1.0.8
|
||||||
LIBXCB_VERSION=1.16
|
LIBXCB_VERSION=1.16.1
|
||||||
BROTLI_VERSION=1.1.0
|
BROTLI_VERSION=1.1.0
|
||||||
|
|
||||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||||
function build_openjpeg {
|
function build_openjpeg {
|
||||||
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
|
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
|
||||||
(cd $out_dir \
|
(cd $out_dir \
|
||||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||||
&& make install)
|
&& make install)
|
||||||
|
@ -62,7 +62,7 @@ function build_brotli {
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||||
export BUILD_PREFIX="/usr/local"
|
sudo chown -R runner /usr/local
|
||||||
fi
|
fi
|
||||||
build_xz
|
build_xz
|
||||||
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
||||||
|
@ -75,8 +75,8 @@ function build {
|
||||||
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
||||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||||
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
|
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||||
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
|
cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||||
|
@ -87,12 +87,10 @@ function build {
|
||||||
build_tiff
|
build_tiff
|
||||||
build_libpng
|
build_libpng
|
||||||
build_lcms2
|
build_lcms2
|
||||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
|
||||||
for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
|
|
||||||
cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
build_openjpeg
|
build_openjpeg
|
||||||
|
if [ -f /usr/local/lib64/libopenjp2.so ]; then
|
||||||
|
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
|
||||||
|
fi
|
||||||
|
|
||||||
ORIGINAL_CFLAGS=$CFLAGS
|
ORIGINAL_CFLAGS=$CFLAGS
|
||||||
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
||||||
|
@ -128,14 +126,19 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de
|
||||||
untar pillow-depends-main.zip
|
untar pillow-depends-main.zip
|
||||||
|
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
|
# libtiff and libxcb cause a conflict with building libtiff and libxcb
|
||||||
# libxau and libxdmcp cause an issue on macOS < 11
|
# libxau and libxdmcp cause an issue on macOS < 11
|
||||||
# if php is installed, brew tries to reinstall these after installing openblas
|
|
||||||
# remove cairo to fix building harfbuzz on arm64
|
# remove cairo to fix building harfbuzz on arm64
|
||||||
# remove lcms2 and libpng to fix building openjpeg on arm64
|
# remove lcms2 and libpng to fix building openjpeg on arm64
|
||||||
# remove zstd to avoid inclusion on x86_64
|
# remove jpeg-turbo to avoid inclusion on arm64
|
||||||
|
# remove webp and zstd to avoid inclusion on x86_64
|
||||||
# curl from brew requires zstd, use system curl
|
# curl from brew requires zstd, use system curl
|
||||||
brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd
|
brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
|
||||||
|
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||||
|
brew remove --ignore-dependencies jpeg-turbo
|
||||||
|
else
|
||||||
|
brew remove --ignore-dependencies webp
|
||||||
|
fi
|
||||||
|
|
||||||
brew install pkg-config
|
brew install pkg-config
|
||||||
fi
|
fi
|
||||||
|
|
3
.github/workflows/wheels-test.sh
vendored
|
@ -4,6 +4,9 @@ set -e
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
brew install fribidi
|
brew install fribidi
|
||||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then
|
||||||
|
sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib
|
||||||
|
fi
|
||||||
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
||||||
apk add curl fribidi
|
apk add curl fribidi
|
||||||
else
|
else
|
||||||
|
|
4
.github/workflows/wheels.yml
vendored
|
@ -99,7 +99,7 @@ jobs:
|
||||||
cibw_arch: x86_64
|
cibw_arch: x86_64
|
||||||
macosx_deployment_target: "10.10"
|
macosx_deployment_target: "10.10"
|
||||||
- name: "macOS arm64"
|
- name: "macOS arm64"
|
||||||
os: macos-latest
|
os: macos-14
|
||||||
cibw_arch: arm64
|
cibw_arch: arm64
|
||||||
macosx_deployment_target: "11.0"
|
macosx_deployment_target: "11.0"
|
||||||
- name: "manylinux2014 and musllinux x86_64"
|
- name: "manylinux2014 and musllinux x86_64"
|
||||||
|
@ -132,7 +132,7 @@ 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_SKIP: pp38-*
|
CIBW_SKIP: pp38-*
|
||||||
CIBW_TEST_SKIP: "*-macosx_arm64"
|
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
|
||||||
|
|
21
CHANGES.rst
|
@ -5,6 +5,27 @@ Changelog (Pillow)
|
||||||
10.3.0 (unreleased)
|
10.3.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Turn off nullability warnings for macOS SDK #7827
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fix shift-sign issue in Convert.c #7838
|
||||||
|
[r-barnes, radarhere]
|
||||||
|
|
||||||
|
- Open 16-bit grayscale PNGs as I;16 #7849
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Handle truncated chunks at the end of PNG images #7709
|
||||||
|
[lajiyuan, radarhere]
|
||||||
|
|
||||||
|
- Match mask size to pasted image size in GifImagePlugin #7779
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
|
||||||
|
[evanmiller, radarhere]
|
||||||
|
|
||||||
|
- Fixed reading FLI/FLC images with a prefix chunk #7804
|
||||||
|
[twolife]
|
||||||
|
|
||||||
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
|
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
|
||||||
[nik012003, radarhere]
|
[nik012003, radarhere]
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ As of 2019, Pillow development is
|
||||||
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
||||||
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
||||||
alt="Tidelift"
|
alt="Tidelift"
|
||||||
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
|
src="https://tidelift.com/badges/package/pypi/pillow?style=flat"></a>
|
||||||
<a href="https://pypi.org/project/pillow/"><img
|
<a href="https://pypi.org/project/pillow/"><img
|
||||||
alt="Newest PyPI version"
|
alt="Newest PyPI version"
|
||||||
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
||||||
|
@ -82,9 +82,6 @@ As of 2019, Pillow development is
|
||||||
<a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img
|
<a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img
|
||||||
alt="Join the chat at https://gitter.im/python-pillow/Pillow"
|
alt="Join the chat at https://gitter.im/python-pillow/Pillow"
|
||||||
src="https://badges.gitter.im/python-pillow/Pillow.svg"></a>
|
src="https://badges.gitter.im/python-pillow/Pillow.svg"></a>
|
||||||
<a href="https://twitter.com/PythonPillow"><img
|
|
||||||
alt="Follow on https://twitter.com/PythonPillow"
|
|
||||||
src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a>
|
|
||||||
<a href="https://fosstodon.org/@pillow"><img
|
<a href="https://fosstodon.org/@pillow"><img
|
||||||
alt="Follow on https://fosstodon.org/@pillow"
|
alt="Follow on https://fosstodon.org/@pillow"
|
||||||
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"
|
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"
|
||||||
|
|
|
@ -86,7 +86,7 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
|
|
||||||
## Publicize Release
|
## Publicize Release
|
||||||
|
|
||||||
* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
|
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,10 @@ def _get_mem_usage() -> float:
|
||||||
|
|
||||||
|
|
||||||
def _test_leak(
|
def _test_leak(
|
||||||
min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
|
min_iterations: int,
|
||||||
|
max_iterations: int,
|
||||||
|
fn: Callable[..., Image.Image | None],
|
||||||
|
*args: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
mem_limit = None
|
mem_limit = None
|
||||||
for i in range(max_iterations):
|
for i in range(max_iterations):
|
||||||
|
|
|
@ -17,6 +17,7 @@ def test_ignore_dos_text() -> None:
|
||||||
finally:
|
finally:
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
for s in im.text.values():
|
for s in im.text.values():
|
||||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ def test_dos_text() -> None:
|
||||||
assert msg, "Decompressed Data Too Large"
|
assert msg, "Decompressed Data Too Large"
|
||||||
return
|
return
|
||||||
|
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
for s in im.text.values():
|
for s in im.text.values():
|
||||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||||
|
|
||||||
|
@ -57,6 +59,7 @@ def test_dos_total_memory() -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
total_len = 0
|
total_len = 0
|
||||||
|
assert isinstance(im2, PngImagePlugin.PngImageFile)
|
||||||
for txt in im2.text.values():
|
for txt in im2.text.values():
|
||||||
total_len += len(txt)
|
total_len += len(txt)
|
||||||
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"
|
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"
|
||||||
|
|
|
@ -244,7 +244,7 @@ def fromstring(data: bytes) -> Image.Image:
|
||||||
return Image.open(BytesIO(data))
|
return Image.open(BytesIO(data))
|
||||||
|
|
||||||
|
|
||||||
def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
|
def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, string_format, **options)
|
im.save(out, string_format, **options)
|
||||||
return out.getvalue()
|
return out.getvalue()
|
||||||
|
@ -351,7 +351,7 @@ def is_mingw() -> bool:
|
||||||
|
|
||||||
|
|
||||||
class CachedProperty:
|
class CachedProperty:
|
||||||
def __init__(self, func: Callable[[Any], None]) -> None:
|
def __init__(self, func: Callable[[Any], Any]) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
|
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
|
||||||
|
|
Before Width: | Height: | Size: 578 B |
BIN
Tests/images/16_bit_binary_pgm.tiff
Normal file
BIN
Tests/images/2422.flc
Normal file
Before Width: | Height: | Size: 298 KiB |
BIN
Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff
Normal file
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 180 B |
BIN
Tests/images/imagedraw_rectangle_I.tiff
Normal file
BIN
Tests/images/negative_top_left_layer.psd
Normal file
BIN
Tests/images/p_8.tga
Normal file
BIN
Tests/images/truncated_end_chunk.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
Tests/images/unknown_compression_method.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from array import array
|
from array import array
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -8,6 +9,7 @@ from PIL import Image, ImageFilter
|
||||||
|
|
||||||
from .helper import assert_image_equal
|
from .helper import assert_image_equal
|
||||||
|
|
||||||
|
numpy: ModuleType | None
|
||||||
try:
|
try:
|
||||||
import numpy
|
import numpy
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -397,6 +399,7 @@ class TestColorLut3DFilter:
|
||||||
|
|
||||||
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
||||||
def test_numpy_sources(self) -> None:
|
def test_numpy_sources(self) -> None:
|
||||||
|
assert numpy is not None
|
||||||
table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
|
table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
|
||||||
with pytest.raises(ValueError, match="should have either channels"):
|
with pytest.raises(ValueError, match="should have either channels"):
|
||||||
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
||||||
|
@ -430,6 +433,7 @@ class TestColorLut3DFilter:
|
||||||
|
|
||||||
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
||||||
def test_numpy_formats(self) -> None:
|
def test_numpy_formats(self) -> None:
|
||||||
|
assert numpy is not None
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
im = Image.merge(
|
im = Image.merge(
|
||||||
"RGB",
|
"RGB",
|
||||||
|
|
|
@ -187,6 +187,6 @@ class TestEnvVars:
|
||||||
{"PILLOW_BLOCKS_MAX": "wat"},
|
{"PILLOW_BLOCKS_MAX": "wat"},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_warnings(self, var) -> None:
|
def test_warnings(self, var: dict[str, str]) -> None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
Image._apply_env_variables(var)
|
Image._apply_env_variables(var)
|
||||||
|
|
|
@ -20,7 +20,7 @@ from PIL import _deprecate
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_version(version, expected) -> None:
|
def test_version(version: int | None, expected: str) -> None:
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old thing", version, "new thing")
|
_deprecate.deprecate("Old thing", version, "new thing")
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ def test_unknown_version() -> None:
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_old_version(deprecated, plural, expected) -> None:
|
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
|
||||||
expected = r""
|
expected = r""
|
||||||
with pytest.raises(RuntimeError, match=expected):
|
with pytest.raises(RuntimeError, match=expected):
|
||||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||||
|
@ -76,7 +76,7 @@ def test_replacement_and_action() -> None:
|
||||||
"Upgrade to new thing.",
|
"Upgrade to new thing.",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_action(action) -> None:
|
def test_action(action: str) -> None:
|
||||||
expected = (
|
expected = (
|
||||||
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||||
r"Upgrade to new thing\."
|
r"Upgrade to new thing\."
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ def test_version() -> None:
|
||||||
# Check the correctness of the convenience function
|
# Check the correctness of the convenience function
|
||||||
# and the format of version numbers
|
# and the format of version numbers
|
||||||
|
|
||||||
def test(name, function) -> None:
|
def test(name: str, function: Callable[[str], bool]) -> None:
|
||||||
version = features.version(name)
|
version = features.version(name)
|
||||||
if not features.check(name):
|
if not features.check(name):
|
||||||
assert version is None
|
assert version is None
|
||||||
|
@ -73,12 +74,12 @@ def test_libimagequant_version() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("feature", features.modules)
|
@pytest.mark.parametrize("feature", features.modules)
|
||||||
def test_check_modules(feature) -> None:
|
def test_check_modules(feature: str) -> None:
|
||||||
assert features.check_module(feature) in [True, False]
|
assert features.check_module(feature) in [True, False]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("feature", features.codecs)
|
@pytest.mark.parametrize("feature", features.codecs)
|
||||||
def test_check_codecs(feature) -> None:
|
def test_check_codecs(feature: str) -> None:
|
||||||
assert features.check_codec(feature) in [True, False]
|
assert features.check_codec(feature) in [True, False]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -721,6 +721,16 @@ def test_save_all_progress() -> None:
|
||||||
assert progress == expected
|
assert progress == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_apng_save_size(tmp_path: Path) -> None:
|
||||||
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
|
im = Image.new("L", (100, 100))
|
||||||
|
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
|
||||||
|
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert reloaded.size == (200, 200)
|
||||||
|
|
||||||
|
|
||||||
def test_seek_after_close() -> None:
|
def test_seek_after_close() -> None:
|
||||||
im = Image.open("Tests/images/apng/delay.png")
|
im = Image.open("Tests/images/apng/delay.png")
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
|
|
@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None:
|
||||||
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crashes(test_file) -> None:
|
def test_crashes(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -16,7 +16,7 @@ from .helper import (
|
||||||
|
|
||||||
|
|
||||||
def test_sanity(tmp_path: Path) -> None:
|
def test_sanity(tmp_path: Path) -> None:
|
||||||
def roundtrip(im) -> None:
|
def roundtrip(im: Image.Image) -> None:
|
||||||
outfile = str(tmp_path / "temp.bmp")
|
outfile = str(tmp_path / "temp.bmp")
|
||||||
|
|
||||||
im.save(outfile, "BMP")
|
im.save(outfile, "BMP")
|
||||||
|
@ -194,7 +194,7 @@ def test_rle4() -> None:
|
||||||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rle8_eof(file_name, length) -> None:
|
def test_rle8_eof(file_name: str, length: int) -> None:
|
||||||
with open(file_name, "rb") as fp:
|
with open(file_name, "rb") as fp:
|
||||||
data = fp.read(length)
|
data = fp.read(length)
|
||||||
with Image.open(io.BytesIO(data)) as im:
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import ContainerIO, Image
|
from PIL import ContainerIO, Image
|
||||||
|
@ -21,9 +23,16 @@ def test_isatty() -> None:
|
||||||
assert container.isatty() is False
|
assert container.isatty() is False
|
||||||
|
|
||||||
|
|
||||||
def test_seek_mode_0() -> None:
|
@pytest.mark.parametrize(
|
||||||
|
"mode, expected_position",
|
||||||
|
(
|
||||||
|
(0, 33),
|
||||||
|
(1, 66),
|
||||||
|
(2, 100),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = 0
|
|
||||||
with open(TEST_FILE, "rb") as fh:
|
with open(TEST_FILE, "rb") as fh:
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||||
|
|
||||||
|
@ -32,35 +41,7 @@ def test_seek_mode_0() -> None:
|
||||||
container.seek(33, mode)
|
container.seek(33, mode)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert container.tell() == 33
|
assert container.tell() == expected_position
|
||||||
|
|
||||||
|
|
||||||
def test_seek_mode_1() -> None:
|
|
||||||
# Arrange
|
|
||||||
mode = 1
|
|
||||||
with open(TEST_FILE, "rb") as fh:
|
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
|
||||||
|
|
||||||
# Act
|
|
||||||
container.seek(33, mode)
|
|
||||||
container.seek(33, mode)
|
|
||||||
|
|
||||||
# Assert
|
|
||||||
assert container.tell() == 66
|
|
||||||
|
|
||||||
|
|
||||||
def test_seek_mode_2() -> None:
|
|
||||||
# Arrange
|
|
||||||
mode = 2
|
|
||||||
with open(TEST_FILE, "rb") as fh:
|
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
|
||||||
|
|
||||||
# Act
|
|
||||||
container.seek(33, mode)
|
|
||||||
container.seek(33, mode)
|
|
||||||
|
|
||||||
# Assert
|
|
||||||
assert container.tell() == 100
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
|
|
|
@ -48,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
||||||
TEST_FILE_DX10_BC1_TYPELESS,
|
TEST_FILE_DX10_BC1_TYPELESS,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_sanity_dxt1_bc1(image_path) -> None:
|
def test_sanity_dxt1_bc1(image_path: str) -> None:
|
||||||
"""Check DXT1 and BC1 images can be opened"""
|
"""Check DXT1 and BC1 images can be opened"""
|
||||||
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
|
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
|
||||||
target = target.convert("RGBA")
|
target = target.convert("RGBA")
|
||||||
|
@ -96,7 +96,7 @@ def test_sanity_dxt5() -> None:
|
||||||
TEST_FILE_BC4U,
|
TEST_FILE_BC4U,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_sanity_ati1_bc4u(image_path) -> None:
|
def test_sanity_ati1_bc4u(image_path: str) -> None:
|
||||||
"""Check ATI1 and BC4U images can be opened"""
|
"""Check ATI1 and BC4U images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -117,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path) -> None:
|
||||||
TEST_FILE_DX10_BC4_TYPELESS,
|
TEST_FILE_DX10_BC4_TYPELESS,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dx10_bc4(image_path) -> None:
|
def test_dx10_bc4(image_path: str) -> None:
|
||||||
"""Check DX10 BC4 images can be opened"""
|
"""Check DX10 BC4 images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -138,7 +138,7 @@ def test_dx10_bc4(image_path) -> None:
|
||||||
TEST_FILE_BC5U,
|
TEST_FILE_BC5U,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_sanity_ati2_bc5u(image_path) -> None:
|
def test_sanity_ati2_bc5u(image_path: str) -> None:
|
||||||
"""Check ATI2 and BC5U images can be opened"""
|
"""Check ATI2 and BC5U images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -162,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path) -> None:
|
||||||
(TEST_FILE_BC5S, TEST_FILE_BC5S),
|
(TEST_FILE_BC5S, TEST_FILE_BC5S),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dx10_bc5(image_path, expected_path) -> None:
|
def test_dx10_bc5(image_path: str, expected_path: str) -> None:
|
||||||
"""Check DX10 BC5 images can be opened"""
|
"""Check DX10 BC5 images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -176,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
|
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
|
||||||
def test_dx10_bc6h(image_path) -> None:
|
def test_dx10_bc6h(image_path: str) -> None:
|
||||||
"""Check DX10 BC6H/BC6HS images can be opened"""
|
"""Check DX10 BC6H/BC6HS images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -257,7 +257,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None:
|
||||||
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
|
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_uncompressed(mode, size, test_file) -> None:
|
def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None:
|
||||||
"""Check uncompressed images can be opened"""
|
"""Check uncompressed images can be opened"""
|
||||||
|
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
@ -359,7 +359,7 @@ def test_unsupported_bitcount() -> None:
|
||||||
"Tests/images/unimplemented_pfflags.dds",
|
"Tests/images/unimplemented_pfflags.dds",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_not_implemented(test_file) -> None:
|
def test_not_implemented(test_file: str) -> None:
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
with Image.open(test_file):
|
with Image.open(test_file):
|
||||||
pass
|
pass
|
||||||
|
@ -381,7 +381,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
|
||||||
("RGBA", "Tests/images/pil123rgba.png"),
|
("RGBA", "Tests/images/pil123rgba.png"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_save(mode, test_file, tmp_path: Path) -> None:
|
def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.dds")
|
out = str(tmp_path / "temp.dds")
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.mode == mode
|
assert im.mode == mode
|
||||||
|
|
|
@ -437,3 +437,11 @@ def test_eof_before_bounding_box() -> None:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_data_after_eof() -> None:
|
||||||
|
with open("Tests/images/illuCS6_preview.eps", "rb") as f:
|
||||||
|
img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255))
|
||||||
|
|
||||||
|
with Image.open(img_bytes) as img:
|
||||||
|
assert img.mode == "RGB"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import FliImagePlugin, Image
|
from PIL import FliImagePlugin, Image, ImageFile
|
||||||
|
|
||||||
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
||||||
|
|
||||||
|
@ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
||||||
# save as...-> hopper.fli, default options.
|
# save as...-> hopper.fli, default options.
|
||||||
static_test_file = "Tests/images/hopper.fli"
|
static_test_file = "Tests/images/hopper.fli"
|
||||||
|
|
||||||
# From https://samples.libav.org/fli-flc/
|
# From https://samples.ffmpeg.org/fli-flc/
|
||||||
animated_test_file = "Tests/images/a.fli"
|
animated_test_file = "Tests/images/a.fli"
|
||||||
|
|
||||||
|
# From https://samples.ffmpeg.org/fli-flc/
|
||||||
|
animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
|
||||||
|
|
||||||
|
|
||||||
def test_sanity() -> None:
|
def test_sanity() -> None:
|
||||||
with Image.open(static_test_file) as im:
|
with Image.open(static_test_file) as im:
|
||||||
|
@ -32,6 +35,24 @@ def test_sanity() -> None:
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefix_chunk() -> None:
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
try:
|
||||||
|
with Image.open(animated_test_file_with_prefix_chunk) as im:
|
||||||
|
assert im.mode == "P"
|
||||||
|
assert im.size == (320, 200)
|
||||||
|
assert im.format == "FLI"
|
||||||
|
assert im.info["duration"] == 171
|
||||||
|
assert im.is_animated
|
||||||
|
|
||||||
|
palette = im.getpalette()
|
||||||
|
assert palette[3:6] == [255, 255, 255]
|
||||||
|
assert palette[381:384] == [204, 204, 12]
|
||||||
|
assert palette[765:] == [252, 0, 0]
|
||||||
|
finally:
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||||
def test_unclosed_file() -> None:
|
def test_unclosed_file() -> None:
|
||||||
def open() -> None:
|
def open() -> None:
|
||||||
|
@ -147,7 +168,7 @@ def test_seek() -> None:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.timeout(timeout=3)
|
@pytest.mark.timeout(timeout=3)
|
||||||
def test_timeouts(test_file) -> None:
|
def test_timeouts(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
@ -160,7 +181,7 @@ def test_timeouts(test_file) -> None:
|
||||||
"Tests/images/crash-5762152299364352.fli",
|
"Tests/images/crash-5762152299364352.fli",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crash(test_file) -> None:
|
def test_crash(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -695,6 +695,9 @@ def test_dispose2_palette(tmp_path: Path) -> None:
|
||||||
# Center remains red every frame
|
# Center remains red every frame
|
||||||
assert rgb_img.getpixel((50, 50)) == circle
|
assert rgb_img.getpixel((50, 50)) == circle
|
||||||
|
|
||||||
|
# Check that frame transparency wasn't added unnecessarily
|
||||||
|
assert img._frame_transparency is None
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_diff(tmp_path: Path) -> None:
|
def test_dispose2_diff(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
@ -782,6 +785,25 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
|
||||||
assert im.n_frames == 3
|
assert im.n_frames == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispose2_previous_frame(tmp_path: Path) -> None:
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
|
im = Image.new("P", (100, 100))
|
||||||
|
im.info["transparency"] = 0
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
d.rectangle([(0, 0), (100, 50)], 1)
|
||||||
|
im.putpalette((0, 0, 0, 255, 0, 0))
|
||||||
|
|
||||||
|
im2 = Image.new("P", (100, 100))
|
||||||
|
im2.putpalette((0, 0, 0))
|
||||||
|
|
||||||
|
im.save(out, save_all=True, append_images=[im2], disposal=[0, 2])
|
||||||
|
|
||||||
|
with Image.open(out) as im:
|
||||||
|
im.seek(1)
|
||||||
|
assert im.getpixel((0, 0)) == (0, 0, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_transparency_in_second_frame(tmp_path: Path) -> None:
|
def test_transparency_in_second_frame(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||||
|
@ -1161,6 +1183,21 @@ def test_append_images(tmp_path: Path) -> None:
|
||||||
assert reread.n_frames == 10
|
assert reread.n_frames == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_append_different_size_image(tmp_path: Path) -> None:
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
|
im = Image.new("RGB", (100, 100))
|
||||||
|
bigger_im = Image.new("RGB", (200, 200), "#f00")
|
||||||
|
|
||||||
|
im.save(out, save_all=True, append_images=[bigger_im])
|
||||||
|
|
||||||
|
with Image.open(out) as reread:
|
||||||
|
assert reread.size == (100, 100)
|
||||||
|
|
||||||
|
reread.seek(1)
|
||||||
|
assert reread.size == (100, 100)
|
||||||
|
|
||||||
|
|
||||||
def test_transparent_optimize(tmp_path: Path) -> None:
|
def test_transparent_optimize(tmp_path: Path) -> None:
|
||||||
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
||||||
# transparency.
|
# transparency.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -55,15 +56,15 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
loaded = False
|
loaded = False
|
||||||
saved = False
|
saved = False
|
||||||
|
|
||||||
def open(self, im) -> None:
|
def open(self, im: Image.Image) -> None:
|
||||||
self.opened = True
|
self.opened = True
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im: Image.Image) -> Image.Image:
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename) -> None:
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
handler = TestHandler()
|
handler = TestHandler()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -56,15 +57,15 @@ def test_handler(tmp_path: Path) -> None:
|
||||||
loaded = False
|
loaded = False
|
||||||
saved = False
|
saved = False
|
||||||
|
|
||||||
def open(self, im) -> None:
|
def open(self, im: Image.Image) -> None:
|
||||||
self.opened = True
|
self.opened = True
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im: Image.Image) -> Image.Image:
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename) -> None:
|
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
handler = TestHandler()
|
handler = TestHandler()
|
||||||
|
|
|
@ -38,6 +38,17 @@ def test_black_and_white() -> None:
|
||||||
assert im.size == (16, 16)
|
assert im.size == (16, 16)
|
||||||
|
|
||||||
|
|
||||||
|
def test_palette(tmp_path: Path) -> None:
|
||||||
|
temp_file = str(tmp_path / "temp.ico")
|
||||||
|
|
||||||
|
im = Image.new("P", (16, 16))
|
||||||
|
im.save(temp_file)
|
||||||
|
|
||||||
|
with Image.open(temp_file) as reloaded:
|
||||||
|
assert reloaded.mode == "P"
|
||||||
|
assert reloaded.palette is not None
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file() -> None:
|
def test_invalid_file() -> None:
|
||||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
|
@ -135,7 +146,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
||||||
def test_save_to_bytes_bmp(mode) -> None:
|
def test_save_to_bytes_bmp(mode: str) -> None:
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
|
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
|
||||||
|
|
|
@ -82,7 +82,7 @@ def test_eoferror() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
||||||
def test_roundtrip(mode, tmp_path: Path) -> None:
|
def test_roundtrip(mode: str, tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.im")
|
out = str(tmp_path / "temp.im")
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
|
@ -98,7 +98,7 @@ def test_i() -> None:
|
||||||
assert ret == 97
|
assert ret == 97
|
||||||
|
|
||||||
|
|
||||||
def test_dump(monkeypatch) -> None:
|
def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
c = b"abc"
|
c = b"abc"
|
||||||
# Temporarily redirect stdout
|
# Temporarily redirect stdout
|
||||||
|
|
|
@ -5,7 +5,8 @@ import re
|
||||||
import warnings
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from types import ModuleType
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ from .helper import (
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ElementTree: ModuleType | None
|
||||||
try:
|
try:
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -43,14 +45,20 @@ TEST_FILE = "Tests/images/hopper.jpg"
|
||||||
|
|
||||||
@skip_unless_feature("jpg")
|
@skip_unless_feature("jpg")
|
||||||
class TestFileJpeg:
|
class TestFileJpeg:
|
||||||
def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
|
def roundtrip_with_bytes(
|
||||||
|
self, im: Image.Image, **options: Any
|
||||||
|
) -> tuple[JpegImagePlugin.JpegImageFile, int]:
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, "JPEG", **options)
|
im.save(out, "JPEG", **options)
|
||||||
test_bytes = out.tell()
|
test_bytes = out.tell()
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
im = Image.open(out)
|
reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out))
|
||||||
im.bytes = test_bytes # for testing only
|
return reloaded, test_bytes
|
||||||
return im
|
|
||||||
|
def roundtrip(
|
||||||
|
self, im: Image.Image, **options: Any
|
||||||
|
) -> JpegImagePlugin.JpegImageFile:
|
||||||
|
return self.roundtrip_with_bytes(im, **options)[0]
|
||||||
|
|
||||||
def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image:
|
def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image:
|
||||||
"""Generates a very hard to compress file
|
"""Generates a very hard to compress file
|
||||||
|
@ -244,13 +252,13 @@ class TestFileJpeg:
|
||||||
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
|
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
|
||||||
|
|
||||||
def test_optimize(self) -> None:
|
def test_optimize(self) -> None:
|
||||||
im1 = self.roundtrip(hopper())
|
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||||
im2 = self.roundtrip(hopper(), optimize=0)
|
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0)
|
||||||
im3 = self.roundtrip(hopper(), optimize=1)
|
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1)
|
||||||
assert_image_equal(im1, im2)
|
assert_image_equal(im1, im2)
|
||||||
assert_image_equal(im1, im3)
|
assert_image_equal(im1, im3)
|
||||||
assert im1.bytes >= im2.bytes
|
assert im1_bytes >= im2_bytes
|
||||||
assert im1.bytes >= im3.bytes
|
assert im1_bytes >= im3_bytes
|
||||||
|
|
||||||
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
|
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
|
||||||
# https://github.com/python-pillow/Pillow/issues/148
|
# https://github.com/python-pillow/Pillow/issues/148
|
||||||
|
@ -260,15 +268,15 @@ class TestFileJpeg:
|
||||||
im.save(f, format="JPEG", optimize=True)
|
im.save(f, format="JPEG", optimize=True)
|
||||||
|
|
||||||
def test_progressive(self) -> None:
|
def test_progressive(self) -> None:
|
||||||
im1 = self.roundtrip(hopper())
|
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||||
im2 = self.roundtrip(hopper(), progressive=False)
|
im2 = self.roundtrip(hopper(), progressive=False)
|
||||||
im3 = self.roundtrip(hopper(), progressive=True)
|
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True)
|
||||||
assert not im1.info.get("progressive")
|
assert not im1.info.get("progressive")
|
||||||
assert not im2.info.get("progressive")
|
assert not im2.info.get("progressive")
|
||||||
assert im3.info.get("progressive")
|
assert im3.info.get("progressive")
|
||||||
|
|
||||||
assert_image_equal(im1, im3)
|
assert_image_equal(im1, im3)
|
||||||
assert im1.bytes >= im3.bytes
|
assert im1_bytes >= im3_bytes
|
||||||
|
|
||||||
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
|
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
|
||||||
f = str(tmp_path / "temp.jpg")
|
f = str(tmp_path / "temp.jpg")
|
||||||
|
@ -339,6 +347,7 @@ class TestFileJpeg:
|
||||||
assert exif.get_ifd(0x8825) == {}
|
assert exif.get_ifd(0x8825) == {}
|
||||||
|
|
||||||
transposed = ImageOps.exif_transpose(im)
|
transposed = ImageOps.exif_transpose(im)
|
||||||
|
assert transposed is not None
|
||||||
exif = transposed.getexif()
|
exif = transposed.getexif()
|
||||||
assert exif.get_ifd(0x8825) == {}
|
assert exif.get_ifd(0x8825) == {}
|
||||||
|
|
||||||
|
@ -417,14 +426,14 @@ class TestFileJpeg:
|
||||||
assert im3.info.get("progression")
|
assert im3.info.get("progression")
|
||||||
|
|
||||||
def test_quality(self) -> None:
|
def test_quality(self) -> None:
|
||||||
im1 = self.roundtrip(hopper())
|
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||||
im2 = self.roundtrip(hopper(), quality=50)
|
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50)
|
||||||
assert_image(im1, im2.mode, im2.size)
|
assert_image(im1, im2.mode, im2.size)
|
||||||
assert im1.bytes >= im2.bytes
|
assert im1_bytes >= im2_bytes
|
||||||
|
|
||||||
im3 = self.roundtrip(hopper(), quality=0)
|
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0)
|
||||||
assert_image(im1, im3.mode, im3.size)
|
assert_image(im1, im3.mode, im3.size)
|
||||||
assert im2.bytes > im3.bytes
|
assert im2_bytes > im3_bytes
|
||||||
|
|
||||||
def test_smooth(self) -> None:
|
def test_smooth(self) -> None:
|
||||||
im1 = self.roundtrip(hopper())
|
im1 = self.roundtrip(hopper())
|
||||||
|
@ -440,25 +449,25 @@ class TestFileJpeg:
|
||||||
for subsampling in (-1, 3): # (default, invalid)
|
for subsampling in (-1, 3): # (default, invalid)
|
||||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
im = self.roundtrip(hopper(), subsampling=subsampling)
|
||||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||||
for subsampling in (0, "4:4:4"):
|
for subsampling1 in (0, "4:4:4"):
|
||||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
im = self.roundtrip(hopper(), subsampling=subsampling1)
|
||||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||||
for subsampling in (1, "4:2:2"):
|
for subsampling1 in (1, "4:2:2"):
|
||||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
im = self.roundtrip(hopper(), subsampling=subsampling1)
|
||||||
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
assert getsampling(im) == (2, 1, 1, 1, 1, 1)
|
||||||
for subsampling in (2, "4:2:0", "4:1:1"):
|
for subsampling1 in (2, "4:2:0", "4:1:1"):
|
||||||
im = self.roundtrip(hopper(), subsampling=subsampling)
|
im = self.roundtrip(hopper(), subsampling=subsampling1)
|
||||||
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
assert getsampling(im) == (2, 2, 1, 1, 1, 1)
|
||||||
|
|
||||||
# RGB colorspace
|
# RGB colorspace
|
||||||
for subsampling in (-1, 0, "4:4:4"):
|
for subsampling1 in (-1, 0, "4:4:4"):
|
||||||
# "4:4:4" doesn't really make sense for RGB, but the conversion
|
# "4:4:4" doesn't really make sense for RGB, but the conversion
|
||||||
# to an integer happens at a higher level
|
# to an integer happens at a higher level
|
||||||
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1)
|
||||||
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
|
||||||
for subsampling in (1, "4:2:2", 2, "4:2:0", 3):
|
for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3):
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
|
self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1)
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
self.roundtrip(hopper(), subsampling="1:1:1")
|
self.roundtrip(hopper(), subsampling="1:1:1")
|
||||||
|
@ -984,13 +993,7 @@ class TestFileJpeg:
|
||||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
|
||||||
decoder = InfiniteMockPyDecoder(None)
|
Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
|
||||||
|
|
||||||
def closure(mode: str, *args) -> InfiniteMockPyDecoder:
|
|
||||||
decoder.__init__(mode, *args)
|
|
||||||
return decoder
|
|
||||||
|
|
||||||
Image.register_decoder("INFINITE", closure)
|
|
||||||
|
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
im.tile = [
|
im.tile = [
|
||||||
|
|
|
@ -40,10 +40,8 @@ test_card.load()
|
||||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, "JPEG2000", **options)
|
im.save(out, "JPEG2000", **options)
|
||||||
test_bytes = out.tell()
|
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
im.bytes = test_bytes # for testing only
|
|
||||||
im.load()
|
im.load()
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
@ -77,7 +75,9 @@ def test_invalid_file() -> None:
|
||||||
def test_bytesio() -> None:
|
def test_bytesio() -> None:
|
||||||
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
|
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
|
||||||
data = BytesIO(f.read())
|
data = BytesIO(f.read())
|
||||||
assert_image_similar_tofile(test_card, data, 1.0e-3)
|
with Image.open(data) as im:
|
||||||
|
im.load()
|
||||||
|
assert_image_similar(im, test_card, 1.0e-3)
|
||||||
|
|
||||||
|
|
||||||
# These two test pre-written JPEG 2000 files that were not written with
|
# These two test pre-written JPEG 2000 files that were not written with
|
||||||
|
@ -340,6 +340,7 @@ def test_parser_feed() -> None:
|
||||||
p.feed(data)
|
p.feed(data)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
assert p.image is not None
|
||||||
assert p.image.size == (640, 480)
|
assert p.image.size == (640, 480)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ from .helper import (
|
||||||
|
|
||||||
@skip_unless_feature("libtiff")
|
@skip_unless_feature("libtiff")
|
||||||
class LibTiffTestCase:
|
class LibTiffTestCase:
|
||||||
def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None:
|
def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
|
||||||
"""Helper tests that assert basic sanity about the g4 tiff reading"""
|
"""Helper tests that assert basic sanity about the g4 tiff reading"""
|
||||||
# 1 bit
|
# 1 bit
|
||||||
assert im.mode == "1"
|
assert im.mode == "1"
|
||||||
|
@ -524,7 +524,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im.save(out, compression=compression)
|
im.save(out, compression=compression)
|
||||||
|
|
||||||
def test_fp_leak(self) -> None:
|
def test_fp_leak(self) -> None:
|
||||||
im = Image.open("Tests/images/hopper_g4_500.tif")
|
im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
|
||||||
|
assert im is not None
|
||||||
fn = im.fp.fileno()
|
fn = im.fp.fileno()
|
||||||
|
|
||||||
os.fstat(fn)
|
os.fstat(fn)
|
||||||
|
@ -716,6 +717,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
f.write(src.read())
|
f.write(src.read())
|
||||||
|
|
||||||
im = Image.open(tmpfile)
|
im = Image.open(tmpfile)
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.n_frames
|
im.n_frames
|
||||||
im.close()
|
im.close()
|
||||||
# Should not raise PermissionError.
|
# Should not raise PermissionError.
|
||||||
|
@ -1097,6 +1099,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
|
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
# Assert that there are multiple strips
|
# Assert that there are multiple strips
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
||||||
|
|
||||||
@pytest.mark.parametrize("argument", (True, False))
|
@pytest.mark.parametrize("argument", (True, False))
|
||||||
|
@ -1113,6 +1116,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im.save(out, **arguments)
|
im.save(out, **arguments)
|
||||||
|
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
||||||
finally:
|
finally:
|
||||||
TiffImagePlugin.STRIP_SIZE = 65536
|
TiffImagePlugin.STRIP_SIZE = 65536
|
||||||
|
|
|
@ -19,7 +19,7 @@ def test_valid_file() -> None:
|
||||||
# https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8
|
# https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8
|
||||||
# https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/
|
# https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/
|
||||||
test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara"
|
test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara"
|
||||||
saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png"
|
saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
|
|
@ -2,11 +2,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, MpoImagePlugin
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -20,14 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
||||||
pytestmark = skip_unless_feature("jpg")
|
pytestmark = skip_unless_feature("jpg")
|
||||||
|
|
||||||
|
|
||||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, "MPO", **options)
|
im.save(out, "MPO", **options)
|
||||||
test_bytes = out.tell()
|
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
im = Image.open(out)
|
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
|
||||||
im.bytes = test_bytes # for testing only
|
|
||||||
return im
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_file", test_files)
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
|
|
|
@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
|
||||||
assert isinstance(im, MspImagePlugin.MspImageFile)
|
assert isinstance(im, MspImagePlugin.MspImageFile)
|
||||||
|
|
||||||
|
|
||||||
def _assert_file_image_equal(source_path, target_path) -> None:
|
def _assert_file_image_equal(source_path: str, target_path: str) -> None:
|
||||||
with Image.open(source_path) as im:
|
with Image.open(source_path) as im:
|
||||||
assert_image_equal_tofile(im, target_path)
|
assert_image_equal_tofile(im, target_path)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from PIL import Image
|
||||||
from .helper import assert_image_equal, hopper, magick_command
|
from .helper import assert_image_equal, hopper, magick_command
|
||||||
|
|
||||||
|
|
||||||
def helper_save_as_palm(tmp_path: Path, mode) -> None:
|
def helper_save_as_palm(tmp_path: Path, mode: str) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
outfile = str(tmp_path / ("temp_" + mode + ".palm"))
|
outfile = str(tmp_path / ("temp_" + mode + ".palm"))
|
||||||
|
@ -24,7 +24,7 @@ def helper_save_as_palm(tmp_path: Path, mode) -> None:
|
||||||
assert os.path.getsize(outfile) > 0
|
assert os.path.getsize(outfile) > 0
|
||||||
|
|
||||||
|
|
||||||
def open_with_magick(magick, tmp_path: Path, f):
|
def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image:
|
||||||
outfile = str(tmp_path / "temp.png")
|
outfile = str(tmp_path / "temp.png")
|
||||||
rc = subprocess.call(
|
rc = subprocess.call(
|
||||||
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
||||||
|
@ -33,7 +33,7 @@ def open_with_magick(magick, tmp_path: Path, f):
|
||||||
return Image.open(outfile)
|
return Image.open(outfile)
|
||||||
|
|
||||||
|
|
||||||
def roundtrip(tmp_path: Path, mode) -> None:
|
def roundtrip(tmp_path: Path, mode: str) -> None:
|
||||||
magick = magick_command()
|
magick = magick_command()
|
||||||
if not magick:
|
if not magick:
|
||||||
return
|
return
|
||||||
|
@ -66,6 +66,6 @@ def test_p_mode(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "RGB"))
|
@pytest.mark.parametrize("mode", ("L", "RGB"))
|
||||||
def test_oserror(tmp_path: Path, mode) -> None:
|
def test_oserror(tmp_path: Path, mode: str) -> None:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
helper_save_as_palm(tmp_path, mode)
|
helper_save_as_palm(tmp_path, mode)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
|
||||||
|
|
||||||
def _roundtrip(tmp_path: Path, im) -> None:
|
def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
|
||||||
f = str(tmp_path / "temp.pcx")
|
f = str(tmp_path / "temp.pcx")
|
||||||
im.save(f)
|
im.save(f)
|
||||||
with Image.open(f) as im2:
|
with Image.open(f) as im2:
|
||||||
|
@ -44,7 +44,7 @@ def test_invalid_file() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
||||||
def test_odd(tmp_path: Path, mode) -> None:
|
def test_odd(tmp_path: Path, mode: str) -> None:
|
||||||
# See issue #523, odd sized images should have a stride that's even.
|
# See issue #523, odd sized images should have a stride that's even.
|
||||||
# Not that ImageMagick or GIMP write PCX that way.
|
# Not that ImageMagick or GIMP write PCX that way.
|
||||||
# We were not handling properly.
|
# We were not handling properly.
|
||||||
|
@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None:
|
||||||
_roundtrip(tmp_path, im)
|
_roundtrip(tmp_path, im)
|
||||||
|
|
||||||
|
|
||||||
def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
|
def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
|
||||||
_last = ImageFile.MAXBLOCK
|
_last = ImageFile.MAXBLOCK
|
||||||
ImageFile.MAXBLOCK = size
|
ImageFile.MAXBLOCK = size
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -6,6 +6,7 @@ import tempfile
|
||||||
import time
|
import time
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ from PIL import Image, PdfParser, features
|
||||||
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
|
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
|
||||||
|
@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
|
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
|
||||||
def test_save(tmp_path: Path, mode) -> None:
|
def test_save(tmp_path: Path, mode: str) -> None:
|
||||||
helper_save_as_pdf(tmp_path, mode)
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("jpg_2000")
|
@skip_unless_feature("jpg_2000")
|
||||||
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
||||||
def test_save_alpha(tmp_path: Path, mode) -> None:
|
def test_save_alpha(tmp_path: Path, mode: str) -> None:
|
||||||
helper_save_as_pdf(tmp_path, mode)
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None:
|
||||||
{"dpi": (75, 150), "resolution": 200},
|
{"dpi": (75, 150), "resolution": 200},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dpi(params, tmp_path: Path) -> None:
|
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
||||||
outfile = str(tmp_path / "temp.pdf")
|
outfile = str(tmp_path / "temp.pdf")
|
||||||
|
@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None:
|
||||||
assert os.path.getsize(outfile) > 0
|
assert os.path.getsize(outfile) > 0
|
||||||
|
|
||||||
# Test appending using a generator
|
# Test appending using a generator
|
||||||
def im_generator(ims):
|
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||||
yield from ims
|
yield from ims
|
||||||
|
|
||||||
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
||||||
|
@ -268,7 +269,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
|
||||||
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
|
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
|
||||||
|
|
||||||
|
|
||||||
def check_pdf_pages_consistency(pdf) -> None:
|
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
|
||||||
pages_info = pdf.read_indirect(pdf.pages_ref)
|
pages_info = pdf.read_indirect(pdf.pages_ref)
|
||||||
assert b"Parent" not in pages_info
|
assert b"Parent" not in pages_info
|
||||||
assert b"Kids" in pages_info
|
assert b"Kids" in pages_info
|
||||||
|
@ -381,7 +382,7 @@ def test_pdf_append_to_bytesio() -> None:
|
||||||
@pytest.mark.timeout(1)
|
@pytest.mark.timeout(1)
|
||||||
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
|
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
|
||||||
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
|
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
|
||||||
def test_redos(newline) -> None:
|
def test_redos(newline: bytes) -> None:
|
||||||
malicious = b" trailer<<>>" + newline * 3456
|
malicious = b" trailer<<>>" + newline * 3456
|
||||||
|
|
||||||
# This particular exception isn't relevant here.
|
# This particular exception isn't relevant here.
|
||||||
|
|
|
@ -6,7 +6,8 @@ import warnings
|
||||||
import zlib
|
import zlib
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from types import ModuleType
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ from .helper import (
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ElementTree: ModuleType | None
|
||||||
try:
|
try:
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -57,11 +59,11 @@ def load(data: bytes) -> Image.Image:
|
||||||
return Image.open(BytesIO(data))
|
return Image.open(BytesIO(data))
|
||||||
|
|
||||||
|
|
||||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, "PNG", **options)
|
im.save(out, "PNG", **options)
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
return Image.open(out)
|
return cast(PngImagePlugin.PngImageFile, Image.open(out))
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("zlib")
|
@skip_unless_feature("zlib")
|
||||||
|
@ -100,7 +102,7 @@ class TestFilePng:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
if mode in ("I;16", "I;16B"):
|
if mode in ("I", "I;16B"):
|
||||||
reloaded = reloaded.convert(mode)
|
reloaded = reloaded.convert(mode)
|
||||||
assert_image_equal(reloaded, im)
|
assert_image_equal(reloaded, im)
|
||||||
|
|
||||||
|
@ -302,8 +304,8 @@ class TestFilePng:
|
||||||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||||
|
|
||||||
def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
|
def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
|
||||||
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
|
for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items():
|
||||||
in_file = "Tests/images/" + mode.lower() + "_trns.png"
|
in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png"
|
||||||
with Image.open(in_file) as im:
|
with Image.open(in_file) as im:
|
||||||
assert im.mode == mode
|
assert im.mode == mode
|
||||||
assert im.info["transparency"] == 255
|
assert im.info["transparency"] == 255
|
||||||
|
@ -617,6 +619,10 @@ class TestFilePng:
|
||||||
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
||||||
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
||||||
|
|
||||||
|
def test_unknown_compression_method(self) -> None:
|
||||||
|
with pytest.raises(SyntaxError, match="Unknown compression method"):
|
||||||
|
PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
|
||||||
|
|
||||||
def test_padded_idat(self) -> None:
|
def test_padded_idat(self) -> None:
|
||||||
# This image has been manually hexedited
|
# This image has been manually hexedited
|
||||||
# so that the IDAT chunk has padding at the end
|
# so that the IDAT chunk has padding at the end
|
||||||
|
@ -781,6 +787,18 @@ class TestFilePng:
|
||||||
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)
|
||||||
|
|
||||||
|
def test_truncated_end_chunk(self) -> None:
|
||||||
|
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
try:
|
||||||
|
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||||
|
finally:
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
|
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
|
||||||
@skip_unless_feature("zlib")
|
@skip_unless_feature("zlib")
|
||||||
|
|
|
@ -88,7 +88,7 @@ def test_16bit_pgm() -> None:
|
||||||
assert im.size == (20, 100)
|
assert im.size == (20, 100)
|
||||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png")
|
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff")
|
||||||
|
|
||||||
|
|
||||||
def test_16bit_pgm_write(tmp_path: Path) -> None:
|
def test_16bit_pgm_write(tmp_path: Path) -> None:
|
||||||
|
|
|
@ -113,6 +113,11 @@ def test_rgba() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_top_left_layer() -> None:
|
||||||
|
with Image.open("Tests/images/negative_top_left_layer.psd") as im:
|
||||||
|
assert im.layers[0][2] == (-50, -50, 50, 50)
|
||||||
|
|
||||||
|
|
||||||
def test_layer_skip() -> None:
|
def test_layer_skip() -> None:
|
||||||
with Image.open("Tests/images/five_channels.psd") as im:
|
with Image.open("Tests/images/five_channels.psd") as im:
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
@ -157,7 +162,7 @@ def test_combined_larger_than_size() -> None:
|
||||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crashes(test_file, raises) -> None:
|
def test_crashes(test_file: str, raises) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with pytest.raises(raises):
|
with pytest.raises(raises):
|
||||||
with Image.open(f):
|
with Image.open(f):
|
||||||
|
|
|
@ -9,7 +9,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageSequence, SpiderImagePlugin
|
from PIL import Image, ImageSequence, SpiderImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile, hopper, is_pypy
|
from .helper import assert_image_equal, hopper, is_pypy
|
||||||
|
|
||||||
TEST_FILE = "Tests/images/hopper.spider"
|
TEST_FILE = "Tests/images/hopper.spider"
|
||||||
|
|
||||||
|
@ -160,4 +160,5 @@ def test_odd_size() -> None:
|
||||||
im.save(data, format="SPIDER")
|
im.save(data, format="SPIDER")
|
||||||
|
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
assert_image_equal_tofile(im, data)
|
with Image.open(data) as im2:
|
||||||
|
assert_image_equal(im, im2)
|
||||||
|
|
|
@ -19,7 +19,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar"
|
||||||
("jpg", "hopper.jpg", "JPEG"),
|
("jpg", "hopper.jpg", "JPEG"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_sanity(codec, test_path, format) -> None:
|
def test_sanity(codec: str, test_path: str, format: str) -> None:
|
||||||
if features.check(codec):
|
if features.check(codec):
|
||||||
with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
|
with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
|
||||||
with Image.open(tar) as im:
|
with Image.open(tar) as im:
|
||||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
|
||||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", _MODES)
|
@pytest.mark.parametrize("mode", _MODES)
|
||||||
def test_sanity(mode, tmp_path: Path) -> None:
|
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||||
def roundtrip(original_im) -> None:
|
def roundtrip(original_im: Image.Image) -> None:
|
||||||
out = str(tmp_path / "temp.tga")
|
out = str(tmp_path / "temp.tga")
|
||||||
|
|
||||||
original_im.save(out, rle=rle)
|
original_im.save(out, rle=rle)
|
||||||
|
@ -65,6 +65,11 @@ def test_sanity(mode, tmp_path: Path) -> None:
|
||||||
roundtrip(original_im)
|
roundtrip(original_im)
|
||||||
|
|
||||||
|
|
||||||
|
def test_palette_depth_8(tmp_path: Path) -> None:
|
||||||
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
Image.open("Tests/images/p_8.tga")
|
||||||
|
|
||||||
|
|
||||||
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_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
|
||||||
|
@ -133,6 +138,11 @@ def test_small_palette(tmp_path: Path) -> None:
|
||||||
assert reloaded.getpalette() == colors
|
assert reloaded.getpalette() == colors
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_palette() -> None:
|
||||||
|
with Image.open("Tests/images/dilation4.lut") as im:
|
||||||
|
assert im.mode == "L"
|
||||||
|
|
||||||
|
|
||||||
def test_save_wrong_mode(tmp_path: Path) -> None:
|
def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||||
im = hopper("PA")
|
im = hopper("PA")
|
||||||
out = str(tmp_path / "temp.tga")
|
out = str(tmp_path / "temp.tga")
|
||||||
|
|
|
@ -4,6 +4,8 @@ import os
|
||||||
import warnings
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -20,6 +22,7 @@ from .helper import (
|
||||||
is_win32,
|
is_win32,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ElementTree: ModuleType | None
|
||||||
try:
|
try:
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -156,7 +159,7 @@ class TestFileTiff:
|
||||||
"resolution_unit, dpi",
|
"resolution_unit, dpi",
|
||||||
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
||||||
)
|
)
|
||||||
def test_load_float_dpi(self, resolution_unit, dpi) -> None:
|
def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None:
|
||||||
with Image.open(
|
with Image.open(
|
||||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||||
) as im:
|
) as im:
|
||||||
|
@ -284,7 +287,7 @@ class TestFileTiff:
|
||||||
("Tests/images/multipage.tiff", 3),
|
("Tests/images/multipage.tiff", 3),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_n_frames(self, path, n_frames) -> None:
|
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
assert im.n_frames == n_frames
|
assert im.n_frames == n_frames
|
||||||
assert im.is_animated == (n_frames != 1)
|
assert im.is_animated == (n_frames != 1)
|
||||||
|
@ -402,7 +405,7 @@ class TestFileTiff:
|
||||||
assert len_before == len_after + 1
|
assert len_before == len_after + 1
|
||||||
|
|
||||||
@pytest.mark.parametrize("legacy_api", (False, True))
|
@pytest.mark.parametrize("legacy_api", (False, True))
|
||||||
def test_load_byte(self, legacy_api) -> None:
|
def test_load_byte(self, legacy_api: bool) -> None:
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
data = b"abc"
|
data = b"abc"
|
||||||
ret = ifd.load_byte(data, legacy_api)
|
ret = ifd.load_byte(data, legacy_api)
|
||||||
|
@ -431,7 +434,7 @@ class TestFileTiff:
|
||||||
assert 0x8825 in im.tag_v2
|
assert 0x8825 in im.tag_v2
|
||||||
|
|
||||||
def test_exif(self, tmp_path: Path) -> None:
|
def test_exif(self, tmp_path: Path) -> None:
|
||||||
def check_exif(exif) -> None:
|
def check_exif(exif: Image.Exif) -> None:
|
||||||
assert sorted(exif.keys()) == [
|
assert sorted(exif.keys()) == [
|
||||||
256,
|
256,
|
||||||
257,
|
257,
|
||||||
|
@ -511,7 +514,7 @@ class TestFileTiff:
|
||||||
assert im.getexif()[273] == (1408, 1907)
|
assert im.getexif()[273] == (1408, 1907)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||||
def test_photometric(self, mode, tmp_path: Path) -> None:
|
def test_photometric(self, mode: str, tmp_path: Path) -> None:
|
||||||
filename = str(tmp_path / "temp.tif")
|
filename = str(tmp_path / "temp.tif")
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(filename, tiffinfo={262: 0})
|
im.save(filename, tiffinfo={262: 0})
|
||||||
|
@ -620,6 +623,7 @@ class TestFileTiff:
|
||||||
im.save(outfile, tiffinfo={278: 256})
|
im.save(outfile, tiffinfo={278: 256})
|
||||||
|
|
||||||
with Image.open(outfile) as im:
|
with Image.open(outfile) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2[278] == 256
|
assert im.tag_v2[278] == 256
|
||||||
|
|
||||||
def test_strip_raw(self) -> None:
|
def test_strip_raw(self) -> None:
|
||||||
|
@ -660,7 +664,7 @@ class TestFileTiff:
|
||||||
assert_image_equal_tofile(reloaded, infile)
|
assert_image_equal_tofile(reloaded, infile)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
def test_palette(self, mode, tmp_path: Path) -> None:
|
def test_palette(self, mode: str, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
|
@ -689,7 +693,7 @@ class TestFileTiff:
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Test appending using a generator
|
# Test appending using a generator
|
||||||
def im_generator(ims):
|
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||||
yield from ims
|
yield from ims
|
||||||
|
|
||||||
mp = BytesIO()
|
mp = BytesIO()
|
||||||
|
@ -911,7 +915,7 @@ class TestFileTiff:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.timeout(2)
|
@pytest.mark.timeout(2)
|
||||||
def test_oom(self, test_file) -> None:
|
def test_oom(self, test_file: str) -> None:
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open(test_file):
|
with Image.open(test_file):
|
||||||
|
|
|
@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
|
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
|
||||||
def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
|
def test_writing_other_types_to_ascii(
|
||||||
|
value: bytes | int, expected: str, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
tag = TiffTags.TAGS_V2[271]
|
tag = TiffTags.TAGS_V2[271]
|
||||||
|
@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
||||||
def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
|
def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
|
@ -222,14 +224,17 @@ def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
|
||||||
assert reloaded.tag_v2[700] == b"\x01"
|
assert reloaded.tag_v2[700] == b"\x01"
|
||||||
|
|
||||||
|
|
||||||
def test_writing_other_types_to_undefined(tmp_path: Path) -> None:
|
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
||||||
|
def test_writing_other_types_to_undefined(
|
||||||
|
value: int | IFDRational, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
tag = TiffTags.TAGS_V2[33723]
|
tag = TiffTags.TAGS_V2[33723]
|
||||||
assert tag.type == TiffTags.UNDEFINED
|
assert tag.type == TiffTags.UNDEFINED
|
||||||
|
|
||||||
info[33723] = 1
|
info[33723] = value
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tiff")
|
out = str(tmp_path / "temp.tiff")
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ pytestmark = [
|
||||||
skip_unless_feature("webp_mux"),
|
skip_unless_feature("webp_mux"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ElementTree: ModuleType | None
|
||||||
try:
|
try:
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont, _util
|
||||||
|
|
||||||
from .helper import PillowLeakTestCase, skip_unless_feature
|
from .helper import PillowLeakTestCase, features, skip_unless_feature
|
||||||
|
|
||||||
|
original_core = ImageFont.core
|
||||||
|
|
||||||
|
|
||||||
class TestTTypeFontLeak(PillowLeakTestCase):
|
class TestTTypeFontLeak(PillowLeakTestCase):
|
||||||
|
@ -31,5 +33,11 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
|
||||||
mem_limit = 1024 # k
|
mem_limit = 1024 # k
|
||||||
|
|
||||||
def test_leak(self) -> None:
|
def test_leak(self) -> None:
|
||||||
|
if features.check_module("freetype2"):
|
||||||
|
ImageFont.core = _util.DeferredError(ImportError)
|
||||||
|
try:
|
||||||
default_font = ImageFont.load_default()
|
default_font = ImageFont.load_default()
|
||||||
|
finally:
|
||||||
|
ImageFont.core = original_core
|
||||||
|
|
||||||
self._test_font(default_font)
|
self._test_font(default_font)
|
||||||
|
|
|
@ -2,21 +2,18 @@ from __future__ import annotations
|
||||||
|
|
||||||
import colorsys
|
import colorsys
|
||||||
import itertools
|
import itertools
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import assert_image_similar, hopper
|
from .helper import assert_image_similar, hopper
|
||||||
|
|
||||||
|
|
||||||
def int_to_float(i):
|
def int_to_float(i: int) -> float:
|
||||||
return i / 255
|
return i / 255
|
||||||
|
|
||||||
|
|
||||||
def str_to_float(i):
|
def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]:
|
||||||
return ord(i) / 255
|
|
||||||
|
|
||||||
|
|
||||||
def tuple_to_ints(tp):
|
|
||||||
x, y, z = tp
|
x, y, z = tp
|
||||||
return int(x * 255.0), int(y * 255.0), int(z * 255.0)
|
return int(x * 255.0), int(y * 255.0), int(z * 255.0)
|
||||||
|
|
||||||
|
@ -25,7 +22,7 @@ def test_sanity() -> None:
|
||||||
Image.new("HSV", (100, 100))
|
Image.new("HSV", (100, 100))
|
||||||
|
|
||||||
|
|
||||||
def wedge():
|
def wedge() -> Image.Image:
|
||||||
w = Image._wedge()
|
w = Image._wedge()
|
||||||
w90 = w.rotate(90)
|
w90 = w.rotate(90)
|
||||||
|
|
||||||
|
@ -49,7 +46,11 @@ def wedge():
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
def to_xxx_colorsys(im, func, mode):
|
def to_xxx_colorsys(
|
||||||
|
im: Image.Image,
|
||||||
|
func: Callable[[float, float, float], tuple[float, float, float]],
|
||||||
|
mode: str,
|
||||||
|
) -> Image.Image:
|
||||||
# convert the hard way using the library colorsys routines.
|
# convert the hard way using the library colorsys routines.
|
||||||
|
|
||||||
(r, g, b) = im.split()
|
(r, g, b) = im.split()
|
||||||
|
@ -70,11 +71,11 @@ def to_xxx_colorsys(im, func, mode):
|
||||||
return hsv
|
return hsv
|
||||||
|
|
||||||
|
|
||||||
def to_hsv_colorsys(im):
|
def to_hsv_colorsys(im: Image.Image) -> Image.Image:
|
||||||
return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV")
|
return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV")
|
||||||
|
|
||||||
|
|
||||||
def to_rgb_colorsys(im):
|
def to_rgb_colorsys(im: Image.Image) -> Image.Image:
|
||||||
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
|
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ from PIL import (
|
||||||
ExifTags,
|
ExifTags,
|
||||||
Image,
|
Image,
|
||||||
ImageDraw,
|
ImageDraw,
|
||||||
|
ImageFile,
|
||||||
ImagePalette,
|
ImagePalette,
|
||||||
UnidentifiedImageError,
|
UnidentifiedImageError,
|
||||||
features,
|
features,
|
||||||
|
@ -61,11 +63,11 @@ class TestImage:
|
||||||
"HSV",
|
"HSV",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_image_modes_success(self, mode) -> None:
|
def test_image_modes_success(self, mode: str) -> None:
|
||||||
Image.new(mode, (1, 1))
|
Image.new(mode, (1, 1))
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
||||||
def test_image_modes_fail(self, mode) -> None:
|
def test_image_modes_fail(self, mode: str) -> None:
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError) as e:
|
||||||
Image.new(mode, (1, 1))
|
Image.new(mode, (1, 1))
|
||||||
assert str(e.value) == "unrecognized image mode"
|
assert str(e.value) == "unrecognized image mode"
|
||||||
|
@ -100,7 +102,7 @@ class TestImage:
|
||||||
|
|
||||||
def test_repr_pretty(self) -> None:
|
def test_repr_pretty(self) -> None:
|
||||||
class Pretty:
|
class Pretty:
|
||||||
def text(self, text) -> None:
|
def text(self, text: str) -> None:
|
||||||
self.pretty_output = text
|
self.pretty_output = text
|
||||||
|
|
||||||
im = Image.new("L", (100, 100))
|
im = Image.new("L", (100, 100))
|
||||||
|
@ -137,13 +139,13 @@ class TestImage:
|
||||||
assert im.height == 2
|
assert im.height == 2
|
||||||
|
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
im.size = (3, 4)
|
im.size = (3, 4) # type: ignore[misc]
|
||||||
|
|
||||||
def test_set_mode(self) -> None:
|
def test_set_mode(self) -> None:
|
||||||
im = Image.new("RGB", (1, 1))
|
im = Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
im.mode = "P"
|
im.mode = "P" # type: ignore[misc]
|
||||||
|
|
||||||
def test_invalid_image(self) -> None:
|
def test_invalid_image(self) -> None:
|
||||||
im = io.BytesIO(b"")
|
im = io.BytesIO(b"")
|
||||||
|
@ -162,8 +164,6 @@ class TestImage:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_pathlib(self, tmp_path: Path) -> None:
|
def test_pathlib(self, tmp_path: Path) -> None:
|
||||||
from PIL.Image import Path
|
|
||||||
|
|
||||||
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
|
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
assert im.size == (10, 10)
|
assert im.size == (10, 10)
|
||||||
|
@ -184,7 +184,9 @@ class TestImage:
|
||||||
temp_file = str(tmp_path / "temp.jpg")
|
temp_file = str(tmp_path / "temp.jpg")
|
||||||
|
|
||||||
class FP:
|
class FP:
|
||||||
def write(self, b) -> None:
|
name: str
|
||||||
|
|
||||||
|
def write(self, b: bytes) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fp = FP()
|
fp = FP()
|
||||||
|
@ -538,7 +540,7 @@ class TestImage:
|
||||||
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
|
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
|
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
|
||||||
def test_empty_image(self, size) -> None:
|
def test_empty_image(self, size: tuple[int, int]) -> None:
|
||||||
Image.new("RGB", size)
|
Image.new("RGB", size)
|
||||||
|
|
||||||
def test_storage_neg(self) -> None:
|
def test_storage_neg(self) -> None:
|
||||||
|
@ -565,7 +567,7 @@ class TestImage:
|
||||||
Image.linear_gradient(wrong_mode)
|
Image.linear_gradient(wrong_mode)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
||||||
def test_linear_gradient(self, mode) -> None:
|
def test_linear_gradient(self, mode: str) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
target_file = "Tests/images/linear_gradient.png"
|
target_file = "Tests/images/linear_gradient.png"
|
||||||
|
|
||||||
|
@ -590,7 +592,7 @@ class TestImage:
|
||||||
Image.radial_gradient(wrong_mode)
|
Image.radial_gradient(wrong_mode)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
||||||
def test_radial_gradient(self, mode) -> None:
|
def test_radial_gradient(self, mode: str) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
target_file = "Tests/images/radial_gradient.png"
|
target_file = "Tests/images/radial_gradient.png"
|
||||||
|
|
||||||
|
@ -665,7 +667,11 @@ class TestImage:
|
||||||
blank_p.palette = None
|
blank_p.palette = None
|
||||||
blank_pa.palette = None
|
blank_pa.palette = None
|
||||||
|
|
||||||
def _make_new(base_image, image, palette_result=None) -> None:
|
def _make_new(
|
||||||
|
base_image: Image.Image,
|
||||||
|
image: Image.Image,
|
||||||
|
palette_result: ImagePalette.ImagePalette | None = None,
|
||||||
|
) -> None:
|
||||||
new_image = base_image._new(image.im)
|
new_image = base_image._new(image.im)
|
||||||
assert new_image.mode == image.mode
|
assert new_image.mode == image.mode
|
||||||
assert new_image.size == image.size
|
assert new_image.size == image.size
|
||||||
|
@ -680,12 +686,15 @@ class TestImage:
|
||||||
_make_new(im, blank_p, ImagePalette.ImagePalette())
|
_make_new(im, blank_p, ImagePalette.ImagePalette())
|
||||||
_make_new(im, blank_pa, ImagePalette.ImagePalette())
|
_make_new(im, blank_pa, ImagePalette.ImagePalette())
|
||||||
|
|
||||||
def test_p_from_rgb_rgba(self) -> None:
|
@pytest.mark.parametrize(
|
||||||
for mode, color in [
|
"mode, color",
|
||||||
|
(
|
||||||
("RGB", "#DDEEFF"),
|
("RGB", "#DDEEFF"),
|
||||||
("RGB", (221, 238, 255)),
|
("RGB", (221, 238, 255)),
|
||||||
("RGBA", (221, 238, 255, 255)),
|
("RGBA", (221, 238, 255, 255)),
|
||||||
]:
|
),
|
||||||
|
)
|
||||||
|
def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None:
|
||||||
im = Image.new("P", (100, 100), color)
|
im = Image.new("P", (100, 100), color)
|
||||||
expected = Image.new(mode, (100, 100), color)
|
expected = Image.new(mode, (100, 100), color)
|
||||||
assert_image_equal(im.convert(mode), expected)
|
assert_image_equal(im.convert(mode), expected)
|
||||||
|
@ -713,7 +722,7 @@ class TestImage:
|
||||||
def test_load_on_nonexclusive_multiframe(self) -> None:
|
def test_load_on_nonexclusive_multiframe(self) -> None:
|
||||||
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
||||||
|
|
||||||
def act(fp) -> None:
|
def act(fp: IO[bytes]) -> None:
|
||||||
im = Image.open(fp)
|
im = Image.open(fp)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -906,12 +915,12 @@ class TestImage:
|
||||||
assert exif.get_ifd(0xA005)
|
assert exif.get_ifd(0xA005)
|
||||||
|
|
||||||
@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) -> None:
|
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
assert im.tobytes() == b""
|
assert im.tobytes() == b""
|
||||||
|
|
||||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||||
def test_zero_frombytes(self, size) -> None:
|
def test_zero_frombytes(self, size: tuple[int, int]) -> None:
|
||||||
Image.frombytes("RGB", size, b"")
|
Image.frombytes("RGB", size, b"")
|
||||||
|
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
|
@ -996,7 +1005,7 @@ class TestImage:
|
||||||
"01r_00.pcx",
|
"01r_00.pcx",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_overrun(self, path) -> None:
|
def test_overrun(self, path: str) -> None:
|
||||||
"""For overrun completeness, test as:
|
"""For overrun completeness, test as:
|
||||||
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
|
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
|
||||||
"""
|
"""
|
||||||
|
@ -1023,7 +1032,7 @@ class TestImage:
|
||||||
pass
|
pass
|
||||||
assert not hasattr(im, "fp")
|
assert not hasattr(im, "fp")
|
||||||
|
|
||||||
def test_close_graceful(self, caplog) -> None:
|
def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
copy = im.copy()
|
copy = im.copy()
|
||||||
with caplog.at_level(logging.DEBUG):
|
with caplog.at_level(logging.DEBUG):
|
||||||
|
@ -1033,25 +1042,20 @@ class TestImage:
|
||||||
assert im.fp is None
|
assert im.fp is None
|
||||||
|
|
||||||
|
|
||||||
class MockEncoder:
|
class MockEncoder(ImageFile.PyEncoder):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def mock_encode(*args):
|
|
||||||
encoder = MockEncoder()
|
|
||||||
encoder.args = args
|
|
||||||
return encoder
|
|
||||||
|
|
||||||
|
|
||||||
class TestRegistry:
|
class TestRegistry:
|
||||||
def test_encode_registry(self) -> None:
|
def test_encode_registry(self) -> None:
|
||||||
Image.register_encoder("MOCK", mock_encode)
|
Image.register_encoder("MOCK", MockEncoder)
|
||||||
assert "MOCK" in Image.ENCODERS
|
assert "MOCK" in Image.ENCODERS
|
||||||
|
|
||||||
enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",))
|
enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",))
|
||||||
|
|
||||||
assert isinstance(enc, MockEncoder)
|
assert isinstance(enc, MockEncoder)
|
||||||
assert enc.args == ("RGB", "args", "extra")
|
assert enc.mode == "RGB"
|
||||||
|
assert enc.args == ("args", "extra")
|
||||||
|
|
||||||
def test_encode_registry_fail(self) -> None:
|
def test_encode_registry_fail(self) -> None:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ from .helper import assert_image_equal, hopper, is_win32
|
||||||
|
|
||||||
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
|
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
|
||||||
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
|
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
|
||||||
|
cffi: ModuleType | None
|
||||||
if os.environ.get("PYTHONOPTIMIZE") == "2":
|
if os.environ.get("PYTHONOPTIMIZE") == "2":
|
||||||
cffi = None
|
cffi = None
|
||||||
else:
|
else:
|
||||||
|
@ -23,6 +25,7 @@ else:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
cffi = None
|
cffi = None
|
||||||
|
|
||||||
|
numpy: ModuleType | None
|
||||||
try:
|
try:
|
||||||
import numpy
|
import numpy
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -71,9 +74,10 @@ class TestImagePutPixel(AccessTest):
|
||||||
pix1 = im1.load()
|
pix1 = im1.load()
|
||||||
pix2 = im2.load()
|
pix2 = im2.load()
|
||||||
|
|
||||||
for x, y in ((0, "0"), ("0", 0)):
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
pix1[x, y]
|
pix1[0, "0"]
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
pix1["0", 0]
|
||||||
|
|
||||||
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]):
|
||||||
|
@ -123,12 +127,13 @@ class TestImagePutPixel(AccessTest):
|
||||||
im = hopper()
|
im = hopper()
|
||||||
pix = im.load()
|
pix = im.load()
|
||||||
|
|
||||||
|
assert numpy is not None
|
||||||
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
|
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
|
||||||
|
|
||||||
|
|
||||||
class TestImageGetPixel(AccessTest):
|
class TestImageGetPixel(AccessTest):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def color(mode):
|
def color(mode: str) -> int | tuple[int, ...]:
|
||||||
bands = Image.getmodebands(mode)
|
bands = Image.getmodebands(mode)
|
||||||
if bands == 1:
|
if bands == 1:
|
||||||
return 1
|
return 1
|
||||||
|
@ -138,12 +143,13 @@ class TestImageGetPixel(AccessTest):
|
||||||
return (16, 32, 49)
|
return (16, 32, 49)
|
||||||
return tuple(range(1, bands + 1))
|
return tuple(range(1, bands + 1))
|
||||||
|
|
||||||
def check(self, mode, expected_color=None) -> None:
|
def check(self, mode: str, expected_color_int: int | None = None) -> None:
|
||||||
if self._need_cffi_access and mode.startswith("BGR;"):
|
if self._need_cffi_access and mode.startswith("BGR;"):
|
||||||
pytest.skip("Support not added to deprecated module for BGR;* modes")
|
pytest.skip("Support not added to deprecated module for BGR;* modes")
|
||||||
|
|
||||||
if not expected_color:
|
expected_color = (
|
||||||
expected_color = self.color(mode)
|
self.color(mode) if expected_color_int is None else expected_color_int
|
||||||
|
)
|
||||||
|
|
||||||
# check putpixel
|
# check putpixel
|
||||||
im = Image.new(mode, (1, 1), None)
|
im = Image.new(mode, (1, 1), None)
|
||||||
|
@ -222,7 +228,7 @@ class TestImageGetPixel(AccessTest):
|
||||||
"YCbCr",
|
"YCbCr",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_basic(self, mode) -> None:
|
def test_basic(self, mode: str) -> None:
|
||||||
self.check(mode)
|
self.check(mode)
|
||||||
|
|
||||||
def test_list(self) -> None:
|
def test_list(self) -> None:
|
||||||
|
@ -231,14 +237,14 @@ class TestImageGetPixel(AccessTest):
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||||
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
|
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
|
||||||
def test_signedness(self, mode, expected_color) -> None:
|
def test_signedness(self, mode: str, expected_color: int) -> None:
|
||||||
# see https://github.com/python-pillow/Pillow/issues/452
|
# see https://github.com/python-pillow/Pillow/issues/452
|
||||||
# pixelaccess is using signed int* instead of uint*
|
# pixelaccess is using signed int* instead of uint*
|
||||||
self.check(mode, expected_color)
|
self.check(mode, expected_color)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
||||||
def test_p_putpixel_rgb_rgba(self, mode, color) -> None:
|
def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None:
|
||||||
im = Image.new(mode, (1, 1))
|
im = Image.new(mode, (1, 1))
|
||||||
im.putpixel((0, 0), color)
|
im.putpixel((0, 0), color)
|
||||||
|
|
||||||
|
@ -262,7 +268,7 @@ class TestCffiGetPixel(TestImageGetPixel):
|
||||||
class TestCffi(AccessTest):
|
class TestCffi(AccessTest):
|
||||||
_need_cffi_access = True
|
_need_cffi_access = True
|
||||||
|
|
||||||
def _test_get_access(self, im) -> None:
|
def _test_get_access(self, im: Image.Image) -> None:
|
||||||
"""Do we get the same thing as the old pixel access
|
"""Do we get the same thing as the old pixel access
|
||||||
|
|
||||||
Using private interfaces, forcing a capi access and
|
Using private interfaces, forcing a capi access and
|
||||||
|
@ -299,7 +305,7 @@ class TestCffi(AccessTest):
|
||||||
# im = Image.new('I;32B', (10, 10), 2**10)
|
# im = Image.new('I;32B', (10, 10), 2**10)
|
||||||
# self._test_get_access(im)
|
# self._test_get_access(im)
|
||||||
|
|
||||||
def _test_set_access(self, im, color) -> None:
|
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
|
||||||
"""Are we writing the correct bits into the image?
|
"""Are we writing the correct bits into the image?
|
||||||
|
|
||||||
Using private interfaces, forcing a capi access and
|
Using private interfaces, forcing a capi access and
|
||||||
|
@ -359,7 +365,7 @@ class TestCffi(AccessTest):
|
||||||
assert px[i, 0] == 0
|
assert px[i, 0] == 0
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
def test_p_putpixel_rgb_rgba(self, mode) -> None:
|
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)):
|
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
|
||||||
im = Image.new(mode, (1, 1))
|
im = Image.new(mode, (1, 1))
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
|
@ -377,7 +383,7 @@ class TestImagePutPixelError(AccessTest):
|
||||||
INVALID_TYPES = ["foo", 1.0, None]
|
INVALID_TYPES = ["foo", 1.0, None]
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", IMAGE_MODES1)
|
@pytest.mark.parametrize("mode", IMAGE_MODES1)
|
||||||
def test_putpixel_type_error1(self, mode) -> None:
|
def test_putpixel_type_error1(self, mode: str) -> None:
|
||||||
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"):
|
||||||
|
@ -400,14 +406,16 @@ class TestImagePutPixelError(AccessTest):
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None:
|
def test_putpixel_invalid_number_of_bands(
|
||||||
|
self, mode: str, band_numbers: tuple[int, ...], match: str
|
||||||
|
) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
for band_number in band_numbers:
|
for band_number in band_numbers:
|
||||||
with pytest.raises(TypeError, match=match):
|
with pytest.raises(TypeError, match=match):
|
||||||
im.putpixel((0, 0), (0,) * band_number)
|
im.putpixel((0, 0), (0,) * band_number)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", IMAGE_MODES2)
|
@pytest.mark.parametrize("mode", IMAGE_MODES2)
|
||||||
def test_putpixel_type_error2(self, mode) -> None:
|
def test_putpixel_type_error2(self, mode: str) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
for v in self.INVALID_TYPES:
|
for v in self.INVALID_TYPES:
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
|
@ -416,7 +424,7 @@ class TestImagePutPixelError(AccessTest):
|
||||||
im.putpixel((0, 0), v)
|
im.putpixel((0, 0), v)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
|
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
|
||||||
def test_putpixel_overflow_error(self, mode) -> None:
|
def test_putpixel_overflow_error(self, mode: str) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
with pytest.raises(OverflowError):
|
with pytest.raises(OverflowError):
|
||||||
im.putpixel((0, 0), 2**80)
|
im.putpixel((0, 0), 2**80)
|
||||||
|
@ -428,7 +436,7 @@ class TestEmbeddable:
|
||||||
def test_embeddable(self) -> None:
|
def test_embeddable(self) -> None:
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
from setuptools.command.build_ext import new_compiler
|
from setuptools.command import build_ext
|
||||||
|
|
||||||
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
||||||
fh.write(
|
fh.write(
|
||||||
|
@ -457,7 +465,7 @@ int main(int argc, char* argv[])
|
||||||
% sys.prefix.replace("\\", "\\\\")
|
% sys.prefix.replace("\\", "\\\\")
|
||||||
)
|
)
|
||||||
|
|
||||||
compiler = new_compiler()
|
compiler = getattr(build_ext, "new_compiler")()
|
||||||
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
|
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
|
||||||
|
|
||||||
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
|
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
|
||||||
|
@ -471,7 +479,7 @@ int main(int argc, char* argv[])
|
||||||
env["PATH"] = sys.prefix + ";" + env["PATH"]
|
env["PATH"] = sys.prefix + ";" + env["PATH"]
|
||||||
|
|
||||||
# do not display the Windows Error Reporting dialog
|
# do not display the Windows Error Reporting dialog
|
||||||
ctypes.windll.kernel32.SetErrorMode(0x0002)
|
getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
|
||||||
|
|
||||||
process = subprocess.Popen(["embed_pil.exe"], env=env)
|
process = subprocess.Popen(["embed_pil.exe"], env=env)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
|
@ -13,7 +15,7 @@ im = hopper().resize((128, 100))
|
||||||
|
|
||||||
|
|
||||||
def test_toarray() -> None:
|
def test_toarray() -> None:
|
||||||
def test(mode):
|
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
|
||||||
|
|
||||||
|
@ -50,14 +52,14 @@ def test_fromarray() -> None:
|
||||||
class Wrapper:
|
class Wrapper:
|
||||||
"""Class with API matching Image.fromarray"""
|
"""Class with API matching Image.fromarray"""
|
||||||
|
|
||||||
def __init__(self, img, arr_params) -> None:
|
def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None:
|
||||||
self.img = img
|
self.img = img
|
||||||
self.__array_interface__ = arr_params
|
self.__array_interface__ = arr_params
|
||||||
|
|
||||||
def tobytes(self):
|
def tobytes(self) -> bytes:
|
||||||
return self.img.tobytes()
|
return self.img.tobytes()
|
||||||
|
|
||||||
def test(mode):
|
def test(mode: str) -> tuple[str, tuple[int, int], bool]:
|
||||||
i = im.convert(mode)
|
i = im.convert(mode)
|
||||||
a = numpy.array(i)
|
a = numpy.array(i)
|
||||||
# Make wrapper instance for image, new array interface
|
# Make wrapper instance for image, new array interface
|
||||||
|
|
|
@ -7,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring
|
||||||
pytestmark = skip_unless_feature("jpg")
|
pytestmark = skip_unless_feature("jpg")
|
||||||
|
|
||||||
|
|
||||||
def draft_roundtrip(in_mode, in_size, req_mode, req_size):
|
def draft_roundtrip(
|
||||||
|
in_mode: str,
|
||||||
|
in_size: tuple[int, int],
|
||||||
|
req_mode: str | None,
|
||||||
|
req_size: tuple[int, int] | None,
|
||||||
|
) -> Image.Image:
|
||||||
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)
|
||||||
|
|
|
@ -4,7 +4,7 @@ from .helper import hopper
|
||||||
|
|
||||||
|
|
||||||
def test_entropy() -> None:
|
def test_entropy() -> None:
|
||||||
def entropy(mode):
|
def entropy(mode: str) -> float:
|
||||||
return hopper(mode).entropy()
|
return hopper(mode).entropy()
|
||||||
|
|
||||||
assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0
|
assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0
|
||||||
|
|
|
@ -36,7 +36,7 @@ from .helper import assert_image_equal, hopper
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
|
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
|
||||||
def test_sanity(filter_to_apply, mode) -> None:
|
def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
|
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
|
||||||
out = im.filter(filter_to_apply)
|
out = im.filter(filter_to_apply)
|
||||||
|
@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
|
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
|
||||||
def test_sanity_error(mode) -> None:
|
def test_sanity_error(mode: str) -> None:
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.filter("hello")
|
im.filter("hello")
|
||||||
|
@ -53,7 +53,7 @@ def test_sanity_error(mode) -> None:
|
||||||
|
|
||||||
# crashes on small images
|
# crashes on small images
|
||||||
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
|
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
|
||||||
def test_crash(size) -> None:
|
def test_crash(size: tuple[int, int]) -> None:
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
im.filter(ImageFilter.SMOOTH)
|
im.filter(ImageFilter.SMOOTH)
|
||||||
|
|
||||||
|
@ -67,7 +67,10 @@ def test_crash(size) -> None:
|
||||||
("RGB", ((4, 0, 0), (0, 0, 0))),
|
("RGB", ((4, 0, 0), (0, 0, 0))),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_modefilter(mode, expected) -> None:
|
def test_modefilter(
|
||||||
|
mode: str,
|
||||||
|
expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]],
|
||||||
|
) -> None:
|
||||||
im = Image.new(mode, (3, 3), None)
|
im = Image.new(mode, (3, 3), None)
|
||||||
im.putdata(list(range(9)))
|
im.putdata(list(range(9)))
|
||||||
# image is:
|
# image is:
|
||||||
|
@ -90,7 +93,13 @@ def test_modefilter(mode, expected) -> None:
|
||||||
("F", (0.0, 4.0, 8.0)),
|
("F", (0.0, 4.0, 8.0)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rankfilter(mode, expected) -> None:
|
def test_rankfilter(
|
||||||
|
mode: str,
|
||||||
|
expected: (
|
||||||
|
tuple[float, float, float]
|
||||||
|
| tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
im = Image.new(mode, (3, 3), None)
|
im = Image.new(mode, (3, 3), None)
|
||||||
im.putdata(list(range(9)))
|
im.putdata(list(range(9)))
|
||||||
# image is:
|
# image is:
|
||||||
|
@ -106,7 +115,7 @@ def test_rankfilter(mode, expected) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
|
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
|
||||||
)
|
)
|
||||||
def test_rankfilter_error(filter) -> None:
|
def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im = Image.new("P", (3, 3), None)
|
im = Image.new("P", (3, 3), None)
|
||||||
im.putdata(list(range(9)))
|
im.putdata(list(range(9)))
|
||||||
|
@ -137,11 +146,9 @@ def test_kernel_not_enough_coefficients() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
||||||
def test_consistency_3x3(mode) -> None:
|
def test_consistency_3x3(mode: str) -> None:
|
||||||
with Image.open("Tests/images/hopper.bmp") as source:
|
with Image.open("Tests/images/hopper.bmp") as source:
|
||||||
reference_name = "hopper_emboss"
|
with Image.open("Tests/images/hopper_emboss.bmp") as reference:
|
||||||
reference_name += "_I.png" if mode == "I" else ".bmp"
|
|
||||||
with Image.open("Tests/images/" + reference_name) as reference:
|
|
||||||
kernel = ImageFilter.Kernel(
|
kernel = ImageFilter.Kernel(
|
||||||
(3, 3),
|
(3, 3),
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
@ -151,23 +158,13 @@ def test_consistency_3x3(mode) -> None:
|
||||||
# fmt: on
|
# fmt: on
|
||||||
0.3,
|
0.3,
|
||||||
)
|
)
|
||||||
source = source.split() * 2
|
|
||||||
reference = reference.split() * 2
|
|
||||||
|
|
||||||
if mode == "I":
|
|
||||||
source = source[0].convert(mode)
|
|
||||||
else:
|
|
||||||
source = Image.merge(mode, source[: len(mode)])
|
|
||||||
reference = Image.merge(mode, reference[: len(mode)])
|
|
||||||
assert_image_equal(source.filter(kernel), reference)
|
assert_image_equal(source.filter(kernel), reference)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
||||||
def test_consistency_5x5(mode) -> None:
|
def test_consistency_5x5(mode: str) -> None:
|
||||||
with Image.open("Tests/images/hopper.bmp") as source:
|
with Image.open("Tests/images/hopper.bmp") as source:
|
||||||
reference_name = "hopper_emboss_more"
|
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
|
||||||
reference_name += "_I.png" if mode == "I" else ".bmp"
|
|
||||||
with Image.open("Tests/images/" + reference_name) as reference:
|
|
||||||
kernel = ImageFilter.Kernel(
|
kernel = ImageFilter.Kernel(
|
||||||
(5, 5),
|
(5, 5),
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
@ -179,14 +176,6 @@ def test_consistency_5x5(mode) -> None:
|
||||||
# fmt: on
|
# fmt: on
|
||||||
0.3,
|
0.3,
|
||||||
)
|
)
|
||||||
source = source.split() * 2
|
|
||||||
reference = reference.split() * 2
|
|
||||||
|
|
||||||
if mode == "I":
|
|
||||||
source = source[0].convert(mode)
|
|
||||||
else:
|
|
||||||
source = Image.merge(mode, source[: len(mode)])
|
|
||||||
reference = Image.merge(mode, reference[: len(mode)])
|
|
||||||
assert_image_equal(source.filter(kernel), reference)
|
assert_image_equal(source.filter(kernel), reference)
|
||||||
|
|
||||||
|
|
||||||
|
@ -199,7 +188,7 @@ def test_consistency_5x5(mode) -> None:
|
||||||
(2, -2),
|
(2, -2),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_invalid_box_blur_filter(radius) -> None:
|
def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFilter.BoxBlur(radius)
|
ImageFilter.BoxBlur(radius)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -17,17 +16,14 @@ pytestmark = pytest.mark.skipif(
|
||||||
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ims = [
|
||||||
@pytest.fixture
|
|
||||||
def test_images() -> Generator[Image.Image, None, None]:
|
|
||||||
ims = [
|
|
||||||
hopper(),
|
hopper(),
|
||||||
Image.open("Tests/images/transparent.png"),
|
Image.open("Tests/images/transparent.png"),
|
||||||
Image.open("Tests/images/7x13.png"),
|
Image.open("Tests/images/7x13.png"),
|
||||||
]
|
]
|
||||||
try:
|
|
||||||
yield ims
|
|
||||||
finally:
|
def teardown_module() -> None:
|
||||||
for im in ims:
|
for im in ims:
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
@ -44,26 +40,26 @@ def roundtrip(expected: Image.Image) -> None:
|
||||||
assert_image_equal(result, expected.convert("RGB"))
|
assert_image_equal(result, expected.convert("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None:
|
def test_sanity_1() -> None:
|
||||||
for im in test_images:
|
for im in ims:
|
||||||
roundtrip(im.convert("1"))
|
roundtrip(im.convert("1"))
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None:
|
def test_sanity_rgb() -> None:
|
||||||
for im in test_images:
|
for im in ims:
|
||||||
roundtrip(im.convert("RGB"))
|
roundtrip(im.convert("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None:
|
def test_sanity_rgba() -> None:
|
||||||
for im in test_images:
|
for im in ims:
|
||||||
roundtrip(im.convert("RGBA"))
|
roundtrip(im.convert("RGBA"))
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None:
|
def test_sanity_l() -> None:
|
||||||
for im in test_images:
|
for im in ims:
|
||||||
roundtrip(im.convert("L"))
|
roundtrip(im.convert("L"))
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None:
|
def test_sanity_p() -> None:
|
||||||
for im in test_images:
|
for im in ims:
|
||||||
roundtrip(im.convert("P"))
|
roundtrip(im.convert("P"))
|
||||||
|
|
|
@ -6,7 +6,7 @@ from .helper import hopper
|
||||||
|
|
||||||
|
|
||||||
def test_extrema() -> None:
|
def test_extrema() -> None:
|
||||||
def extrema(mode):
|
def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]:
|
||||||
return hopper(mode).getextrema()
|
return hopper(mode).getextrema()
|
||||||
|
|
||||||
assert extrema("1") == (0, 255)
|
assert extrema("1") == (0, 255)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from .helper import hopper
|
||||||
|
|
||||||
|
|
||||||
def test_palette() -> None:
|
def test_palette() -> None:
|
||||||
def palette(mode):
|
def palette(mode: str) -> list[int] | None:
|
||||||
p = hopper(mode).getpalette()
|
p = hopper(mode).getpalette()
|
||||||
if p:
|
if p:
|
||||||
return p[:10]
|
return p[:10]
|
||||||
|
|
|
@ -8,7 +8,6 @@ from .helper import CachedProperty, assert_image_equal
|
||||||
|
|
||||||
|
|
||||||
class TestImagingPaste:
|
class TestImagingPaste:
|
||||||
masks = {}
|
|
||||||
size = 128
|
size = 128
|
||||||
|
|
||||||
def assert_9points_image(
|
def assert_9points_image(
|
||||||
|
@ -33,7 +32,7 @@ class TestImagingPaste:
|
||||||
def assert_9points_paste(
|
def assert_9points_paste(
|
||||||
self,
|
self,
|
||||||
im: Image.Image,
|
im: Image.Image,
|
||||||
im2: Image.Image,
|
im2: Image.Image | str | tuple[int, ...],
|
||||||
mask: Image.Image,
|
mask: Image.Image,
|
||||||
expected: list[tuple[int, int, int, int]],
|
expected: list[tuple[int, int, int, int]],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -46,7 +45,7 @@ class TestImagingPaste:
|
||||||
self.assert_9points_image(im, expected)
|
self.assert_9points_image(im, expected)
|
||||||
|
|
||||||
@CachedProperty
|
@CachedProperty
|
||||||
def mask_1(self):
|
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()
|
||||||
for y in range(mask.height):
|
for y in range(mask.height):
|
||||||
|
@ -55,11 +54,11 @@ class TestImagingPaste:
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
@CachedProperty
|
@CachedProperty
|
||||||
def mask_L(self):
|
def mask_L(self) -> Image.Image:
|
||||||
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
|
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
|
||||||
|
|
||||||
@CachedProperty
|
@CachedProperty
|
||||||
def gradient_L(self):
|
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()
|
||||||
for y in range(gradient.height):
|
for y in range(gradient.height):
|
||||||
|
@ -68,7 +67,7 @@ class TestImagingPaste:
|
||||||
return gradient
|
return gradient
|
||||||
|
|
||||||
@CachedProperty
|
@CachedProperty
|
||||||
def gradient_RGB(self):
|
def gradient_RGB(self) -> Image.Image:
|
||||||
return Image.merge(
|
return Image.merge(
|
||||||
"RGB",
|
"RGB",
|
||||||
[
|
[
|
||||||
|
@ -79,7 +78,7 @@ class TestImagingPaste:
|
||||||
)
|
)
|
||||||
|
|
||||||
@CachedProperty
|
@CachedProperty
|
||||||
def gradient_LA(self):
|
def gradient_LA(self) -> Image.Image:
|
||||||
return Image.merge(
|
return Image.merge(
|
||||||
"LA",
|
"LA",
|
||||||
[
|
[
|
||||||
|
@ -89,7 +88,7 @@ class TestImagingPaste:
|
||||||
)
|
)
|
||||||
|
|
||||||
@CachedProperty
|
@CachedProperty
|
||||||
def gradient_RGBA(self):
|
def gradient_RGBA(self) -> Image.Image:
|
||||||
return Image.merge(
|
return Image.merge(
|
||||||
"RGBA",
|
"RGBA",
|
||||||
[
|
[
|
||||||
|
@ -101,7 +100,7 @@ class TestImagingPaste:
|
||||||
)
|
)
|
||||||
|
|
||||||
@CachedProperty
|
@CachedProperty
|
||||||
def gradient_RGBa(self):
|
def gradient_RGBa(self) -> Image.Image:
|
||||||
return Image.merge(
|
return Image.merge(
|
||||||
"RGBa",
|
"RGBa",
|
||||||
[
|
[
|
||||||
|
|
|
@ -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):
|
def put(value: int) -> tuple[int, int, int, int]:
|
||||||
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))
|
||||||
|
@ -58,7 +58,7 @@ def test_mode_with_L_with_float() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
|
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
|
||||||
def test_mode_i(mode) -> None:
|
def test_mode_i(mode: str) -> None:
|
||||||
src = hopper("L")
|
src = hopper("L")
|
||||||
data = list(src.getdata())
|
data = list(src.getdata())
|
||||||
im = Image.new(mode, src.size, 0)
|
im = Image.new(mode, src.size, 0)
|
||||||
|
@ -79,7 +79,7 @@ def test_mode_F() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
|
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
|
||||||
def test_mode_BGR(mode) -> None:
|
def test_mode_BGR(mode: str) -> None:
|
||||||
data = [(16, 32, 49), (32, 32, 98)]
|
data = [(16, 32, 49), (32, 32, 98)]
|
||||||
im = Image.new(mode, (1, 2))
|
im = Image.new(mode, (1, 2))
|
||||||
im.putdata(data)
|
im.putdata(data)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||||
|
|
||||||
|
|
||||||
def test_putpalette() -> None:
|
def test_putpalette() -> None:
|
||||||
def palette(mode):
|
def palette(mode: str) -> str | tuple[str, list[int]]:
|
||||||
im = hopper(mode).copy()
|
im = hopper(mode).copy()
|
||||||
im.putpalette(list(range(256)) * 3)
|
im.putpalette(list(range(256)) * 3)
|
||||||
p = im.getpalette()
|
p = im.getpalette()
|
||||||
|
@ -81,7 +81,7 @@ def test_putpalette_with_alpha_values() -> None:
|
||||||
("RGBAX", (1, 2, 3, 4, 0)),
|
("RGBAX", (1, 2, 3, 4, 0)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rgba_palette(mode, palette) -> None:
|
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
|
||||||
im = Image.new("P", (1, 1))
|
im = Image.new("P", (1, 1))
|
||||||
im.putpalette(palette, mode)
|
im.putpalette(palette, mode)
|
||||||
assert im.getpalette() == [1, 2, 3]
|
assert im.getpalette() == [1, 2, 3]
|
||||||
|
|
|
@ -231,11 +231,13 @@ class TestImagingCoreResampleAccuracy:
|
||||||
|
|
||||||
|
|
||||||
class TestCoreResampleConsistency:
|
class TestCoreResampleConsistency:
|
||||||
def make_case(self, mode: str, fill: tuple[int, int, int] | float):
|
def make_case(
|
||||||
|
self, mode: str, fill: tuple[int, int, int] | float
|
||||||
|
) -> tuple[Image.Image, 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]
|
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
|
||||||
|
|
||||||
def run_case(self, case) -> None:
|
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
|
||||||
channel, color = case
|
channel, color = case
|
||||||
px = channel.load()
|
px = channel.load()
|
||||||
for x in range(channel.size[0]):
|
for x in range(channel.size[0]):
|
||||||
|
@ -353,7 +355,7 @@ class TestCoreResampleAlphaCorrect:
|
||||||
|
|
||||||
class TestCoreResamplePasses:
|
class TestCoreResamplePasses:
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def count(self, diff):
|
def count(self, diff: int) -> Generator[None, None, None]:
|
||||||
count = Image.core.get_stats()["new_count"]
|
count = Image.core.get_stats()["new_count"]
|
||||||
yield
|
yield
|
||||||
assert Image.core.get_stats()["new_count"] - count == diff
|
assert Image.core.get_stats()["new_count"] - count == diff
|
||||||
|
|
|
@ -154,7 +154,7 @@ class TestImagingCoreResize:
|
||||||
|
|
||||||
def test_unknown_filter(self) -> None:
|
def test_unknown_filter(self) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.resize(hopper(), (10, 10), 9)
|
self.resize(hopper(), (10, 10), 9) # type: ignore[arg-type]
|
||||||
|
|
||||||
def test_cross_platform(self, tmp_path: Path) -> None:
|
def test_cross_platform(self, tmp_path: Path) -> None:
|
||||||
# This test is intended for only check for consistent behaviour across
|
# This test is intended for only check for consistent behaviour across
|
||||||
|
|
|
@ -12,7 +12,13 @@ from .helper import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def rotate(im, mode, angle, center=None, translate=None) -> None:
|
def rotate(
|
||||||
|
im: Image.Image,
|
||||||
|
mode: str,
|
||||||
|
angle: int,
|
||||||
|
center: tuple[int, int] | None = None,
|
||||||
|
translate: tuple[int, int] | None = None,
|
||||||
|
) -> None:
|
||||||
out = im.rotate(angle, center=center, translate=translate)
|
out = im.rotate(angle, center=center, translate=translate)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size # default rotate clips output
|
assert out.size == im.size # default rotate clips output
|
||||||
|
@ -27,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
|
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
|
||||||
def test_mode(mode) -> None:
|
def test_mode(mode: str) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
rotate(im, mode, 45)
|
rotate(im, mode, 45)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("angle", (0, 90, 180, 270))
|
@pytest.mark.parametrize("angle", (0, 90, 180, 270))
|
||||||
def test_angle(angle) -> None:
|
def test_angle(angle: int) -> None:
|
||||||
with Image.open("Tests/images/test-card.png") as im:
|
with Image.open("Tests/images/test-card.png") as im:
|
||||||
rotate(im, im.mode, angle)
|
rotate(im, im.mode, angle)
|
||||||
|
|
||||||
|
@ -42,7 +48,7 @@ def test_angle(angle) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
|
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
|
||||||
def test_zero(angle) -> None:
|
def test_zero(angle: int) -> None:
|
||||||
im = Image.new("RGB", (0, 0))
|
im = Image.new("RGB", (0, 0))
|
||||||
rotate(im, im.mode, angle)
|
rotate(im, im.mode, angle)
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ def test_load_first_unless_jpeg() -> None:
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
draft = im.draft
|
draft = im.draft
|
||||||
|
|
||||||
def im_draft(mode, size):
|
def im_draft(mode: str, size: tuple[int, int]):
|
||||||
result = draft(mode, size)
|
result = draft(mode, size)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ class TestImageTransform:
|
||||||
("LA", (76, 0)),
|
("LA", (76, 0)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_fill(self, mode, expected_pixel) -> None:
|
def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
(w, h) = im.size
|
(w, h) = im.size
|
||||||
transformed = im.transform(
|
transformed = im.transform(
|
||||||
|
@ -142,7 +143,9 @@ class TestImageTransform:
|
||||||
assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2)))
|
assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2)))
|
||||||
assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h)))
|
assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h)))
|
||||||
|
|
||||||
def _test_alpha_premult(self, op) -> None:
|
def _test_alpha_premult(
|
||||||
|
self, op: Callable[[Image.Image, tuple[int, int]], Image.Image]
|
||||||
|
) -> None:
|
||||||
# create image with half white, half black,
|
# create image with half white, half black,
|
||||||
# with the black half transparent.
|
# with the black half transparent.
|
||||||
# do op,
|
# do op,
|
||||||
|
@ -159,13 +162,13 @@ class TestImageTransform:
|
||||||
assert 40 * 10 == hist[-1]
|
assert 40 * 10 == hist[-1]
|
||||||
|
|
||||||
def test_alpha_premult_resize(self) -> None:
|
def test_alpha_premult_resize(self) -> None:
|
||||||
def op(im, sz):
|
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
||||||
return im.resize(sz, Image.Resampling.BILINEAR)
|
return im.resize(sz, Image.Resampling.BILINEAR)
|
||||||
|
|
||||||
self._test_alpha_premult(op)
|
self._test_alpha_premult(op)
|
||||||
|
|
||||||
def test_alpha_premult_transform(self) -> None:
|
def test_alpha_premult_transform(self) -> None:
|
||||||
def op(im, sz):
|
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
||||||
(w, h) = im.size
|
(w, h) = im.size
|
||||||
return im.transform(
|
return im.transform(
|
||||||
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
|
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
|
||||||
|
@ -173,7 +176,9 @@ class TestImageTransform:
|
||||||
|
|
||||||
self._test_alpha_premult(op)
|
self._test_alpha_premult(op)
|
||||||
|
|
||||||
def _test_nearest(self, op, mode) -> None:
|
def _test_nearest(
|
||||||
|
self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str
|
||||||
|
) -> None:
|
||||||
# create white image with half transparent,
|
# create white image with half transparent,
|
||||||
# do op,
|
# do op,
|
||||||
# the image should remain white with half transparent
|
# the image should remain white with half transparent
|
||||||
|
@ -196,15 +201,15 @@ class TestImageTransform:
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
|
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
|
||||||
def test_nearest_resize(self, mode) -> None:
|
def test_nearest_resize(self, mode: str) -> None:
|
||||||
def op(im, sz):
|
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
||||||
return im.resize(sz, Image.Resampling.NEAREST)
|
return im.resize(sz, Image.Resampling.NEAREST)
|
||||||
|
|
||||||
self._test_nearest(op, mode)
|
self._test_nearest(op, mode)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
|
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
|
||||||
def test_nearest_transform(self, mode) -> None:
|
def test_nearest_transform(self, mode: str) -> None:
|
||||||
def op(im, sz):
|
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
|
||||||
(w, h) = im.size
|
(w, h) = im.size
|
||||||
return im.transform(
|
return im.transform(
|
||||||
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
|
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
|
||||||
|
@ -227,7 +232,9 @@ class TestImageTransform:
|
||||||
# Running by default, but I'd totally understand not doing it in
|
# Running by default, but I'd totally understand not doing it in
|
||||||
# the future
|
# the future
|
||||||
|
|
||||||
pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)]
|
pattern: list[Image.Image] | None = [
|
||||||
|
Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)
|
||||||
|
]
|
||||||
|
|
||||||
# Yeah. Watch some JIT optimize this out.
|
# Yeah. Watch some JIT optimize this out.
|
||||||
pattern = None # noqa: F841
|
pattern = None # noqa: F841
|
||||||
|
@ -240,7 +247,7 @@ class TestImageTransform:
|
||||||
im.transform((100, 100), None)
|
im.transform((100, 100), None)
|
||||||
|
|
||||||
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
|
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
|
||||||
def test_unknown_resampling_filter(self, resample) -> None:
|
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
(w, h) = im.size
|
(w, h) = im.size
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -250,7 +257,7 @@ class TestImageTransform:
|
||||||
class TestImageTransformAffine:
|
class TestImageTransformAffine:
|
||||||
transform = Image.Transform.AFFINE
|
transform = Image.Transform.AFFINE
|
||||||
|
|
||||||
def _test_image(self):
|
def _test_image(self) -> Image.Image:
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
return im.crop((10, 20, im.width - 10, im.height - 20))
|
return im.crop((10, 20, im.width - 10, im.height - 20))
|
||||||
|
|
||||||
|
@ -263,7 +270,7 @@ class TestImageTransformAffine:
|
||||||
(270, Image.Transpose.ROTATE_270),
|
(270, Image.Transpose.ROTATE_270),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rotate(self, deg, transpose) -> None:
|
def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None:
|
||||||
im = self._test_image()
|
im = self._test_image()
|
||||||
|
|
||||||
angle = -math.radians(deg)
|
angle = -math.radians(deg)
|
||||||
|
@ -313,7 +320,13 @@ class TestImageTransformAffine:
|
||||||
(Image.Resampling.BICUBIC, 1),
|
(Image.Resampling.BICUBIC, 1),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None:
|
def test_resize(
|
||||||
|
self,
|
||||||
|
scale: float,
|
||||||
|
epsilon_scale: float,
|
||||||
|
resample: Image.Resampling,
|
||||||
|
epsilon: int,
|
||||||
|
) -> None:
|
||||||
im = self._test_image()
|
im = self._test_image()
|
||||||
|
|
||||||
size_up = int(round(im.width * scale)), int(round(im.height * scale))
|
size_up = int(round(im.width * scale)), int(round(im.height * scale))
|
||||||
|
@ -342,7 +355,14 @@ class TestImageTransformAffine:
|
||||||
(Image.Resampling.BICUBIC, 1),
|
(Image.Resampling.BICUBIC, 1),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None:
|
def test_translate(
|
||||||
|
self,
|
||||||
|
x: float,
|
||||||
|
y: float,
|
||||||
|
epsilon_scale: float,
|
||||||
|
resample: Image.Resampling,
|
||||||
|
epsilon: float,
|
||||||
|
) -> None:
|
||||||
im = self._test_image()
|
im = self._test_image()
|
||||||
|
|
||||||
size_up = int(round(im.width + x)), int(round(im.height + y))
|
size_up = int(round(im.width + x)), int(round(im.height + y))
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from PIL import Image, ImageChops
|
from PIL import Image, ImageChops
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
@ -387,7 +389,9 @@ def test_overlay() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_logical() -> None:
|
def test_logical() -> None:
|
||||||
def table(op, a, b):
|
def table(
|
||||||
|
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
|
||||||
|
) -> tuple[int, int, int, int]:
|
||||||
out = []
|
out = []
|
||||||
for x in (a, b):
|
for x in (a, b):
|
||||||
imx = Image.new("1", (1, 1), x)
|
imx = Image.new("1", (1, 1), x)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("flag", ("my string", -1))
|
@pytest.mark.parametrize("flag", ("my string", -1))
|
||||||
def test_invalid_flag(flag) -> None:
|
def test_invalid_flag(flag: str | int) -> None:
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
|
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
|
||||||
|
@ -335,19 +336,21 @@ def test_extended_information() -> None:
|
||||||
o = ImageCms.getOpenProfile(SRGB)
|
o = ImageCms.getOpenProfile(SRGB)
|
||||||
p = o.profile
|
p = o.profile
|
||||||
|
|
||||||
def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None:
|
def assert_truncated_tuple_equal(
|
||||||
|
tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10
|
||||||
|
) -> None:
|
||||||
# Helper function to reduce precision of tuples of floats
|
# Helper function to reduce precision of tuples of floats
|
||||||
# recursively and then check equality.
|
# recursively and then check equality.
|
||||||
power = 10**digits
|
power = 10**digits
|
||||||
|
|
||||||
def truncate_tuple(tuple_or_float):
|
def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]:
|
||||||
return tuple(
|
return tuple(
|
||||||
(
|
(
|
||||||
truncate_tuple(val)
|
truncate_tuple(val)
|
||||||
if isinstance(val, tuple)
|
if isinstance(val, tuple)
|
||||||
else int(val * power) / power
|
else int(val * power) / power
|
||||||
)
|
)
|
||||||
for val in tuple_or_float
|
for val in tuple_value
|
||||||
)
|
)
|
||||||
|
|
||||||
assert truncate_tuple(tup1) == truncate_tuple(tup2)
|
assert truncate_tuple(tup1) == truncate_tuple(tup2)
|
||||||
|
@ -504,8 +507,10 @@ def test_profile_typesafety() -> None:
|
||||||
ImageCms.ImageCmsProfile(1).tobytes()
|
ImageCms.ImageCmsProfile(1).tobytes()
|
||||||
|
|
||||||
|
|
||||||
def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None:
|
def assert_aux_channel_preserved(
|
||||||
def create_test_image():
|
mode: str, transform_in_place: bool, preserved_channel: str
|
||||||
|
) -> None:
|
||||||
|
def create_test_image() -> Image.Image:
|
||||||
# set up test image with something interesting in the tested aux channel.
|
# set up test image with something interesting in the tested aux channel.
|
||||||
# fmt: off
|
# fmt: off
|
||||||
nine_grid_deltas = [
|
nine_grid_deltas = [
|
||||||
|
@ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
||||||
def test_rgb_lab(mode) -> None:
|
def test_rgb_lab(mode: str) -> None:
|
||||||
im = Image.new(mode, (1, 1))
|
im = Image.new(mode, (1, 1))
|
||||||
converted_im = im.convert("LAB")
|
converted_im = im.convert("LAB")
|
||||||
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
||||||
|
|
|
@ -753,7 +753,7 @@ def test_rectangle_I16(bbox: Coords) -> None:
|
||||||
draw.rectangle(bbox, outline=0xFFFF)
|
draw.rectangle(bbox, outline=0xFFFF)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import os.path
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageDraw2, features
|
from PIL import Image, ImageDraw, ImageDraw2, features
|
||||||
|
from PIL._typing import Coords
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -56,7 +57,7 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
def test_ellipse(bbox) -> None:
|
def test_ellipse(bbox: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -84,7 +85,7 @@ def test_ellipse_edge() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("points", POINTS)
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
def test_line(points) -> None:
|
def test_line(points: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -98,7 +99,7 @@ def test_line(points) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("points", POINTS)
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
def test_line_pen_as_brush(points) -> None:
|
def test_line_pen_as_brush(points: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("points", POINTS)
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
def test_polygon(points) -> None:
|
def test_polygon(points: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
@ -129,7 +130,7 @@ def test_polygon(points) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bbox", BBOX)
|
@pytest.mark.parametrize("bbox", BBOX)
|
||||||
def test_rectangle(bbox) -> None:
|
def test_rectangle(bbox: Coords) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw2.Draw(im)
|
draw = ImageDraw2.Draw(im)
|
||||||
|
|
|
@ -22,7 +22,7 @@ def test_crash() -> None:
|
||||||
ImageEnhance.Sharpness(im).enhance(0.5)
|
ImageEnhance.Sharpness(im).enhance(0.5)
|
||||||
|
|
||||||
|
|
||||||
def _half_transparent_image():
|
def _half_transparent_image() -> Image.Image:
|
||||||
# returns an image, half transparent, half solid
|
# returns an image, half transparent, half solid
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
|
|
||||||
|
@ -34,7 +34,9 @@ def _half_transparent_image():
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
def _check_alpha(im, original, op, amount) -> None:
|
def _check_alpha(
|
||||||
|
im: Image.Image, original: Image.Image, op: str, amount: float
|
||||||
|
) -> None:
|
||||||
assert im.getbands() == original.getbands()
|
assert im.getbands() == original.getbands()
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im.getchannel("A"),
|
im.getchannel("A"),
|
||||||
|
@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
|
@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
|
||||||
def test_alpha(op) -> None:
|
def test_alpha(op: str) -> None:
|
||||||
# Issue https://github.com/python-pillow/Pillow/issues/899
|
# Issue https://github.com/python-pillow/Pillow/issues/899
|
||||||
# Is alpha preserved through image enhancement?
|
# Is alpha preserved through image enhancement?
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
|
||||||
|
|
||||||
class TestImageFile:
|
class TestImageFile:
|
||||||
def test_parser(self) -> None:
|
def test_parser(self) -> None:
|
||||||
def roundtrip(format):
|
def roundtrip(format: str) -> tuple[Image.Image, Image.Image]:
|
||||||
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
|
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
|
||||||
if format in ("MSP", "XBM"):
|
if format in ("MSP", "XBM"):
|
||||||
im = im.convert("1")
|
im = im.convert("1")
|
||||||
|
@ -201,12 +202,22 @@ class TestImageFile:
|
||||||
|
|
||||||
|
|
||||||
class MockPyDecoder(ImageFile.PyDecoder):
|
class MockPyDecoder(ImageFile.PyDecoder):
|
||||||
|
def __init__(self, mode: str, *args: Any) -> None:
|
||||||
|
MockPyDecoder.last = self
|
||||||
|
|
||||||
|
super().__init__(mode, *args)
|
||||||
|
|
||||||
def decode(self, buffer):
|
def decode(self, buffer):
|
||||||
# eof
|
# eof
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
class MockPyEncoder(ImageFile.PyEncoder):
|
class MockPyEncoder(ImageFile.PyEncoder):
|
||||||
|
def __init__(self, mode: str, *args: Any) -> None:
|
||||||
|
MockPyEncoder.last = self
|
||||||
|
|
||||||
|
super().__init__(mode, *args)
|
||||||
|
|
||||||
def encode(self, buffer):
|
def encode(self, buffer):
|
||||||
return 1, 1, b""
|
return 1, 1, b""
|
||||||
|
|
||||||
|
@ -228,19 +239,8 @@ class MockImageFile(ImageFile.ImageFile):
|
||||||
class CodecsTest:
|
class CodecsTest:
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls) -> None:
|
def setup_class(cls) -> None:
|
||||||
cls.decoder = MockPyDecoder(None)
|
Image.register_decoder("MOCK", MockPyDecoder)
|
||||||
cls.encoder = MockPyEncoder(None)
|
Image.register_encoder("MOCK", MockPyEncoder)
|
||||||
|
|
||||||
def decoder_closure(mode, *args):
|
|
||||||
cls.decoder.__init__(mode, *args)
|
|
||||||
return cls.decoder
|
|
||||||
|
|
||||||
def encoder_closure(mode, *args):
|
|
||||||
cls.encoder.__init__(mode, *args)
|
|
||||||
return cls.encoder
|
|
||||||
|
|
||||||
Image.register_decoder("MOCK", decoder_closure)
|
|
||||||
Image.register_encoder("MOCK", encoder_closure)
|
|
||||||
|
|
||||||
|
|
||||||
class TestPyDecoder(CodecsTest):
|
class TestPyDecoder(CodecsTest):
|
||||||
|
@ -251,13 +251,13 @@ class TestPyDecoder(CodecsTest):
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
assert self.decoder.state.xoff == xoff
|
assert MockPyDecoder.last.state.xoff == xoff
|
||||||
assert self.decoder.state.yoff == yoff
|
assert MockPyDecoder.last.state.yoff == yoff
|
||||||
assert self.decoder.state.xsize == xsize
|
assert MockPyDecoder.last.state.xsize == xsize
|
||||||
assert self.decoder.state.ysize == ysize
|
assert MockPyDecoder.last.state.ysize == ysize
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.decoder.set_as_raw(b"\x00")
|
MockPyDecoder.last.set_as_raw(b"\x00")
|
||||||
|
|
||||||
def test_extents_none(self) -> None:
|
def test_extents_none(self) -> None:
|
||||||
buf = BytesIO(b"\x00" * 255)
|
buf = BytesIO(b"\x00" * 255)
|
||||||
|
@ -267,10 +267,10 @@ class TestPyDecoder(CodecsTest):
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
assert self.decoder.state.xoff == 0
|
assert MockPyDecoder.last.state.xoff == 0
|
||||||
assert self.decoder.state.yoff == 0
|
assert MockPyDecoder.last.state.yoff == 0
|
||||||
assert self.decoder.state.xsize == 200
|
assert MockPyDecoder.last.state.xsize == 200
|
||||||
assert self.decoder.state.ysize == 200
|
assert MockPyDecoder.last.state.ysize == 200
|
||||||
|
|
||||||
def test_negsize(self) -> None:
|
def test_negsize(self) -> None:
|
||||||
buf = BytesIO(b"\x00" * 255)
|
buf = BytesIO(b"\x00" * 255)
|
||||||
|
@ -315,10 +315,10 @@ class TestPyEncoder(CodecsTest):
|
||||||
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
|
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert self.encoder.state.xoff == xoff
|
assert MockPyEncoder.last.state.xoff == xoff
|
||||||
assert self.encoder.state.yoff == yoff
|
assert MockPyEncoder.last.state.yoff == yoff
|
||||||
assert self.encoder.state.xsize == xsize
|
assert MockPyEncoder.last.state.xsize == xsize
|
||||||
assert self.encoder.state.ysize == ysize
|
assert MockPyEncoder.last.state.ysize == ysize
|
||||||
|
|
||||||
def test_extents_none(self) -> None:
|
def test_extents_none(self) -> None:
|
||||||
buf = BytesIO(b"\x00" * 255)
|
buf = BytesIO(b"\x00" * 255)
|
||||||
|
@ -329,10 +329,10 @@ class TestPyEncoder(CodecsTest):
|
||||||
fp = BytesIO()
|
fp = BytesIO()
|
||||||
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
|
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
|
||||||
|
|
||||||
assert self.encoder.state.xoff == 0
|
assert MockPyEncoder.last.state.xoff == 0
|
||||||
assert self.encoder.state.yoff == 0
|
assert MockPyEncoder.last.state.yoff == 0
|
||||||
assert self.encoder.state.xsize == 200
|
assert MockPyEncoder.last.state.xsize == 200
|
||||||
assert self.encoder.state.ysize == 200
|
assert MockPyEncoder.last.state.ysize == 200
|
||||||
|
|
||||||
def test_negsize(self) -> None:
|
def test_negsize(self) -> None:
|
||||||
buf = BytesIO(b"\x00" * 255)
|
buf = BytesIO(b"\x00" * 255)
|
||||||
|
@ -340,12 +340,12 @@ class TestPyEncoder(CodecsTest):
|
||||||
im = MockImageFile(buf)
|
im = MockImageFile(buf)
|
||||||
|
|
||||||
fp = BytesIO()
|
fp = BytesIO()
|
||||||
self.encoder.cleanup_called = False
|
MockPyEncoder.last = None
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
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 self.encoder.cleanup_called
|
assert MockPyEncoder.last.cleanup_called
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFile._save(
|
ImageFile._save(
|
||||||
|
|
|
@ -7,11 +7,13 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, BinaryIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFont, features
|
from PIL import Image, ImageDraw, ImageFont, features
|
||||||
|
from PIL._typing import StrOrBytesPath
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -42,16 +44,16 @@ def test_sanity() -> None:
|
||||||
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
|
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def layout_engine(request):
|
def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def font(layout_engine):
|
def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont:
|
||||||
return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
|
return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_font_properties(font) -> None:
|
def test_font_properties(font: ImageFont.FreeTypeFont) -> None:
|
||||||
assert font.path == FONT_PATH
|
assert font.path == FONT_PATH
|
||||||
assert font.size == FONT_SIZE
|
assert font.size == FONT_SIZE
|
||||||
|
|
||||||
|
@ -67,7 +69,9 @@ def test_font_properties(font) -> None:
|
||||||
assert font_copy.path == second_font_path
|
assert font_copy.path == second_font_path
|
||||||
|
|
||||||
|
|
||||||
def _render(font, layout_engine):
|
def _render(
|
||||||
|
font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout
|
||||||
|
) -> Image.Image:
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
|
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
|
||||||
ttf.getbbox(txt)
|
ttf.getbbox(txt)
|
||||||
|
@ -80,12 +84,12 @@ def _render(font, layout_engine):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
|
@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
|
||||||
def test_font_with_name(layout_engine, font) -> None:
|
def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None:
|
||||||
_render(font, layout_engine)
|
_render(font, layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_font_with_filelike(layout_engine) -> None:
|
def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None:
|
||||||
def _font_as_bytes():
|
def _font_as_bytes() -> BytesIO:
|
||||||
with open(FONT_PATH, "rb") as f:
|
with open(FONT_PATH, "rb") as f:
|
||||||
font_bytes = BytesIO(f.read())
|
font_bytes = BytesIO(f.read())
|
||||||
return font_bytes
|
return font_bytes
|
||||||
|
@ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine) -> None:
|
||||||
# _render(shared_bytes)
|
# _render(shared_bytes)
|
||||||
|
|
||||||
|
|
||||||
def test_font_with_open_file(layout_engine) -> None:
|
def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None:
|
||||||
with open(FONT_PATH, "rb") as f:
|
with open(FONT_PATH, "rb") as f:
|
||||||
_render(f, layout_engine)
|
_render(f, layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_render_equal(layout_engine) -> None:
|
def test_render_equal(layout_engine: ImageFont.Layout) -> None:
|
||||||
img_path = _render(FONT_PATH, layout_engine)
|
img_path = _render(FONT_PATH, layout_engine)
|
||||||
with open(FONT_PATH, "rb") as f:
|
with open(FONT_PATH, "rb") as f:
|
||||||
font_filelike = BytesIO(f.read())
|
font_filelike = BytesIO(f.read())
|
||||||
|
@ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None:
|
||||||
assert_image_equal(img_path, img_filelike)
|
assert_image_equal(img_path, img_filelike)
|
||||||
|
|
||||||
|
|
||||||
def test_non_ascii_path(tmp_path: Path, layout_engine) -> None:
|
def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None:
|
||||||
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
|
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
|
||||||
try:
|
try:
|
||||||
shutil.copy(FONT_PATH, tempfile)
|
shutil.copy(FONT_PATH, tempfile)
|
||||||
|
@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None:
|
||||||
ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
|
ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
|
||||||
def test_transparent_background(font) -> None:
|
def test_transparent_background(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGBA", size=(300, 100))
|
im = Image.new(mode="RGBA", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -140,7 +144,7 @@ def test_transparent_background(font) -> None:
|
||||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
||||||
|
|
||||||
def test_I16(font) -> None:
|
def test_I16(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="I;16", size=(300, 100))
|
im = Image.new(mode="I;16", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -153,7 +157,7 @@ def test_I16(font) -> None:
|
||||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
||||||
|
|
||||||
def test_textbbox_equal(font) -> None:
|
def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_getlength(
|
def test_getlength(
|
||||||
text, mode, fontname, size, layout_engine, length_basic, length_raqm
|
text: str,
|
||||||
|
mode: str,
|
||||||
|
fontname: str,
|
||||||
|
size: int,
|
||||||
|
layout_engine: ImageFont.Layout,
|
||||||
|
length_basic: int,
|
||||||
|
length_raqm: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
|
f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
@ -207,7 +217,7 @@ def test_float_size() -> None:
|
||||||
assert lengths[0] != lengths[1] != lengths[2]
|
assert lengths[0] != lengths[1] != lengths[2]
|
||||||
|
|
||||||
|
|
||||||
def test_render_multiline(font) -> None:
|
def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
line_spacing = font.getbbox("A")[3] + 4
|
line_spacing = font.getbbox("A")[3] + 4
|
||||||
|
@ -223,7 +233,7 @@ def test_render_multiline(font) -> None:
|
||||||
assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
|
assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
|
||||||
|
|
||||||
|
|
||||||
def test_render_multiline_text(font) -> None:
|
def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# Test that text() correctly connects to multiline_text()
|
# Test that text() correctly connects to multiline_text()
|
||||||
# and that align defaults to left
|
# and that align defaults to left
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
|
@ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
|
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
|
||||||
)
|
)
|
||||||
def test_render_multiline_text_align(font, align, ext) -> None:
|
def test_render_multiline_text_align(
|
||||||
|
font: ImageFont.FreeTypeFont, align: str, ext: str
|
||||||
|
) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
|
draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
|
||||||
|
@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None:
|
||||||
assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
|
assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_align(font) -> None:
|
def test_unknown_align(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -260,14 +272,14 @@ def test_unknown_align(font) -> None:
|
||||||
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
|
draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
|
||||||
|
|
||||||
|
|
||||||
def test_draw_align(font) -> None:
|
def test_draw_align(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new("RGB", (300, 100), "white")
|
im = Image.new("RGB", (300, 100), "white")
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
line = "some text"
|
line = "some text"
|
||||||
draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
|
draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
|
||||||
|
|
||||||
|
|
||||||
def test_multiline_bbox(font) -> None:
|
def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None:
|
||||||
draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
|
draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
|
||||||
|
|
||||||
|
|
||||||
def test_multiline_width(font) -> None:
|
def test_multiline_width(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
@ -295,7 +307,7 @@ def test_multiline_width(font) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_multiline_spacing(font) -> None:
|
def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
|
draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
|
||||||
|
@ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
||||||
)
|
)
|
||||||
def test_rotated_transposed_font(font, orientation) -> None:
|
def test_rotated_transposed_font(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
img_gray = Image.new("L", (100, 100))
|
img_gray = Image.new("L", (100, 100))
|
||||||
draw = ImageDraw.Draw(img_gray)
|
draw = ImageDraw.Draw(img_gray)
|
||||||
word = "testing"
|
word = "testing"
|
||||||
|
@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None:
|
||||||
Image.Transpose.FLIP_TOP_BOTTOM,
|
Image.Transpose.FLIP_TOP_BOTTOM,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_unrotated_transposed_font(font, orientation) -> None:
|
def test_unrotated_transposed_font(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
img_gray = Image.new("L", (100, 100))
|
img_gray = Image.new("L", (100, 100))
|
||||||
draw = ImageDraw.Draw(img_gray)
|
draw = ImageDraw.Draw(img_gray)
|
||||||
word = "testing"
|
word = "testing"
|
||||||
|
@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
|
||||||
)
|
)
|
||||||
def test_rotated_transposed_font_get_mask(font, orientation) -> None:
|
def test_rotated_transposed_font_get_mask(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
text = "mask this"
|
text = "mask this"
|
||||||
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
||||||
|
@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None:
|
||||||
Image.Transpose.FLIP_TOP_BOTTOM,
|
Image.Transpose.FLIP_TOP_BOTTOM,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_unrotated_transposed_font_get_mask(font, orientation) -> None:
|
def test_unrotated_transposed_font_get_mask(
|
||||||
|
font: ImageFont.FreeTypeFont, orientation: Image.Transpose
|
||||||
|
) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
text = "mask this"
|
text = "mask this"
|
||||||
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
|
||||||
|
@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None:
|
||||||
assert mask.size == (108, 13)
|
assert mask.size == (108, 13)
|
||||||
|
|
||||||
|
|
||||||
def test_free_type_font_get_name(font) -> None:
|
def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None:
|
||||||
assert ("FreeMono", "Regular") == font.getname()
|
assert ("FreeMono", "Regular") == font.getname()
|
||||||
|
|
||||||
|
|
||||||
def test_free_type_font_get_metrics(font) -> None:
|
def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None:
|
||||||
ascent, descent = font.getmetrics()
|
ascent, descent = font.getmetrics()
|
||||||
|
|
||||||
assert isinstance(ascent, int)
|
assert isinstance(ascent, int)
|
||||||
|
@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None:
|
||||||
assert (ascent, descent) == (16, 4)
|
assert (ascent, descent) == (16, 4)
|
||||||
|
|
||||||
|
|
||||||
def test_free_type_font_get_mask(font) -> None:
|
def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
text = "mask this"
|
text = "mask this"
|
||||||
|
|
||||||
|
@ -473,16 +493,16 @@ def test_default_font() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
|
||||||
def test_getbbox(font, mode) -> None:
|
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None:
|
||||||
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
assert (0, 4, 12, 16) == font.getbbox("A", mode)
|
||||||
|
|
||||||
|
|
||||||
def test_getbbox_empty(font) -> None:
|
def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# issue #2614, should not crash.
|
# issue #2614, should not crash.
|
||||||
assert (0, 0, 0, 0) == font.getbbox("")
|
assert (0, 0, 0, 0) == font.getbbox("")
|
||||||
|
|
||||||
|
|
||||||
def test_render_empty(font) -> None:
|
def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
|
||||||
# issue 2666
|
# issue 2666
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
target = im.copy()
|
target = im.copy()
|
||||||
|
@ -492,7 +512,7 @@ def test_render_empty(font) -> None:
|
||||||
assert_image_equal(im, target)
|
assert_image_equal(im, target)
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_extended(layout_engine) -> None:
|
def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
|
||||||
# issue #3777
|
# issue #3777
|
||||||
text = "A\u278A\U0001F12B"
|
text = "A\u278A\U0001F12B"
|
||||||
target = "Tests/images/unicode_extended.png"
|
target = "Tests/images/unicode_extended.png"
|
||||||
|
@ -515,21 +535,23 @@ def test_unicode_extended(layout_engine) -> None:
|
||||||
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
|
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
|
||||||
)
|
)
|
||||||
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
||||||
def test_find_font(monkeypatch, platform, font_directory) -> None:
|
def test_find_font(
|
||||||
def _test_fake_loading_font(path_to_fake, fontname) -> None:
|
monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str
|
||||||
|
) -> None:
|
||||||
|
def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None:
|
||||||
# Make a copy of FreeTypeFont so we can patch the original
|
# Make a copy of FreeTypeFont so we can patch the original
|
||||||
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
|
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
|
||||||
with monkeypatch.context() as m:
|
with monkeypatch.context() as m:
|
||||||
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
|
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
|
||||||
|
|
||||||
def loadable_font(filepath, size, index, encoding, *args, **kwargs):
|
def loadable_font(
|
||||||
|
filepath: str, size: int, index: int, encoding: str, *args: Any
|
||||||
|
):
|
||||||
if filepath == path_to_fake:
|
if filepath == path_to_fake:
|
||||||
return ImageFont._FreeTypeFont(
|
return ImageFont._FreeTypeFont(
|
||||||
FONT_PATH, size, index, encoding, *args, **kwargs
|
FONT_PATH, size, index, encoding, *args
|
||||||
)
|
|
||||||
return ImageFont._FreeTypeFont(
|
|
||||||
filepath, size, index, encoding, *args, **kwargs
|
|
||||||
)
|
)
|
||||||
|
return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
|
||||||
|
|
||||||
m.setattr(ImageFont, "FreeTypeFont", loadable_font)
|
m.setattr(ImageFont, "FreeTypeFont", loadable_font)
|
||||||
font = ImageFont.truetype(fontname)
|
font = ImageFont.truetype(fontname)
|
||||||
|
@ -543,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
|
||||||
if platform == "linux":
|
if platform == "linux":
|
||||||
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
|
||||||
|
|
||||||
def fake_walker(path):
|
def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
|
||||||
if path == font_directory:
|
if path == font_directory:
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
|
@ -567,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
|
||||||
_test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
|
_test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
|
||||||
|
|
||||||
|
|
||||||
def test_imagefont_getters(font) -> None:
|
def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None:
|
||||||
assert font.getmetrics() == (16, 4)
|
assert font.getmetrics() == (16, 4)
|
||||||
assert font.font.ascent == 16
|
assert font.font.ascent == 16
|
||||||
assert font.font.descent == 4
|
assert font.font.descent == 4
|
||||||
|
@ -588,7 +610,7 @@ def test_imagefont_getters(font) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("stroke_width", (0, 2))
|
@pytest.mark.parametrize("stroke_width", (0, 2))
|
||||||
def test_getsize_stroke(font, stroke_width) -> None:
|
def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None:
|
||||||
assert font.getbbox("A", stroke_width=stroke_width) == (
|
assert font.getbbox("A", stroke_width=stroke_width) == (
|
||||||
0 - stroke_width,
|
0 - stroke_width,
|
||||||
4 - stroke_width,
|
4 - stroke_width,
|
||||||
|
@ -607,7 +629,7 @@ def test_complex_font_settings() -> None:
|
||||||
t.getmask("абвг", language="sr")
|
t.getmask("абвг", language="sr")
|
||||||
|
|
||||||
|
|
||||||
def test_variation_get(font) -> None:
|
def test_variation_get(font: ImageFont.FreeTypeFont) -> None:
|
||||||
freetype = parse_version(features.version_module("freetype2"))
|
freetype = parse_version(features.version_module("freetype2"))
|
||||||
if freetype < parse_version("2.9.1"):
|
if freetype < parse_version("2.9.1"):
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
@ -662,7 +684,7 @@ def test_variation_get(font) -> None:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _check_text(font, path, epsilon):
|
def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None:
|
||||||
im = Image.new("RGB", (100, 75), "white")
|
im = Image.new("RGB", (100, 75), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.text((10, 10), "Text", font=font, fill="black")
|
d.text((10, 10), "Text", font=font, fill="black")
|
||||||
|
@ -677,7 +699,7 @@ def _check_text(font, path, epsilon):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_variation_set_by_name(font) -> None:
|
def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
|
||||||
freetype = parse_version(features.version_module("freetype2"))
|
freetype = parse_version(features.version_module("freetype2"))
|
||||||
if freetype < parse_version("2.9.1"):
|
if freetype < parse_version("2.9.1"):
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
@ -702,7 +724,7 @@ def test_variation_set_by_name(font) -> None:
|
||||||
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
|
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
|
||||||
|
|
||||||
|
|
||||||
def test_variation_set_by_axes(font) -> None:
|
def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None:
|
||||||
freetype = parse_version(features.version_module("freetype2"))
|
freetype = parse_version(features.version_module("freetype2"))
|
||||||
if freetype < parse_version("2.9.1"):
|
if freetype < parse_version("2.9.1"):
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
@ -737,7 +759,9 @@ def test_variation_set_by_axes(font) -> None:
|
||||||
),
|
),
|
||||||
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
|
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
|
||||||
)
|
)
|
||||||
def test_anchor(layout_engine, anchor, left, top) -> None:
|
def test_anchor(
|
||||||
|
layout_engine: ImageFont.Layout, anchor: str, left: int, top: int
|
||||||
|
) -> None:
|
||||||
name, text = "quick", "Quick"
|
name, text = "quick", "Quick"
|
||||||
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
|
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
|
||||||
|
|
||||||
|
@ -782,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None:
|
||||||
("md", "center"),
|
("md", "center"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_anchor_multiline(layout_engine, anchor, align) -> None:
|
def test_anchor_multiline(
|
||||||
|
layout_engine: ImageFont.Layout, anchor: str, align: str
|
||||||
|
) -> None:
|
||||||
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
|
target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
|
||||||
text = "a\nlong\ntext sample"
|
text = "a\nlong\ntext sample"
|
||||||
|
|
||||||
|
@ -800,7 +826,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None:
|
||||||
assert_image_similar_tofile(im, target, 4)
|
assert_image_similar_tofile(im, target, 4)
|
||||||
|
|
||||||
|
|
||||||
def test_anchor_invalid(font) -> None:
|
def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None:
|
||||||
im = Image.new("RGB", (100, 100), "white")
|
im = Image.new("RGB", (100, 100), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.font = font
|
d.font = font
|
||||||
|
@ -826,7 +852,7 @@ def test_anchor_invalid(font) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
|
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
|
||||||
def test_bitmap_font(layout_engine, bpp) -> None:
|
def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None:
|
||||||
text = "Bitmap Font"
|
text = "Bitmap Font"
|
||||||
layout_name = ["basic", "raqm"][layout_engine]
|
layout_name = ["basic", "raqm"][layout_engine]
|
||||||
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
|
target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
|
||||||
|
@ -843,7 +869,7 @@ def test_bitmap_font(layout_engine, bpp) -> None:
|
||||||
assert_image_equal_tofile(im, target)
|
assert_image_equal_tofile(im, target)
|
||||||
|
|
||||||
|
|
||||||
def test_bitmap_font_stroke(layout_engine) -> None:
|
def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None:
|
||||||
text = "Bitmap Font"
|
text = "Bitmap Font"
|
||||||
layout_name = ["basic", "raqm"][layout_engine]
|
layout_name = ["basic", "raqm"][layout_engine]
|
||||||
target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
|
target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
|
||||||
|
@ -861,7 +887,7 @@ def test_bitmap_font_stroke(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("embedded_color", (False, True))
|
@pytest.mark.parametrize("embedded_color", (False, True))
|
||||||
def test_bitmap_blend(layout_engine, embedded_color) -> None:
|
def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
"Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
)
|
)
|
||||||
|
@ -873,7 +899,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
|
assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
|
||||||
|
|
||||||
|
|
||||||
def test_standard_embedded_color(layout_engine) -> None:
|
def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None:
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
||||||
ttf.getbbox(txt)
|
ttf.getbbox(txt)
|
||||||
|
@ -886,7 +912,7 @@ def test_standard_embedded_color(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
|
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
|
||||||
def test_float_coord(layout_engine, fontmode):
|
def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None:
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
||||||
|
|
||||||
|
@ -908,7 +934,7 @@ def test_float_coord(layout_engine, fontmode):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_cbdt(layout_engine) -> None:
|
def test_cbdt(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
|
@ -925,7 +951,7 @@ def test_cbdt(layout_engine) -> None:
|
||||||
pytest.skip("freetype compiled without libpng or CBDT support")
|
pytest.skip("freetype compiled without libpng or CBDT support")
|
||||||
|
|
||||||
|
|
||||||
def test_cbdt_mask(layout_engine) -> None:
|
def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
|
@ -942,7 +968,7 @@ def test_cbdt_mask(layout_engine) -> None:
|
||||||
pytest.skip("freetype compiled without libpng or CBDT support")
|
pytest.skip("freetype compiled without libpng or CBDT support")
|
||||||
|
|
||||||
|
|
||||||
def test_sbix(layout_engine) -> None:
|
def test_sbix(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
||||||
|
@ -959,7 +985,7 @@ def test_sbix(layout_engine) -> None:
|
||||||
pytest.skip("freetype compiled without libpng or SBIX support")
|
pytest.skip("freetype compiled without libpng or SBIX support")
|
||||||
|
|
||||||
|
|
||||||
def test_sbix_mask(layout_engine) -> None:
|
def test_sbix_mask(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
"Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
|
||||||
|
@ -977,7 +1003,7 @@ def test_sbix_mask(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature_version("freetype2", "2.10.0")
|
@skip_unless_feature_version("freetype2", "2.10.0")
|
||||||
def test_colr(layout_engine) -> None:
|
def test_colr(layout_engine: ImageFont.Layout) -> None:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
||||||
size=64,
|
size=64,
|
||||||
|
@ -993,7 +1019,7 @@ def test_colr(layout_engine) -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature_version("freetype2", "2.10.0")
|
@skip_unless_feature_version("freetype2", "2.10.0")
|
||||||
def test_colr_mask(layout_engine) -> None:
|
def test_colr_mask(layout_engine: ImageFont.Layout) -> None:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
"Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
|
||||||
size=64,
|
size=64,
|
||||||
|
@ -1008,7 +1034,7 @@ def test_colr_mask(layout_engine) -> None:
|
||||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
|
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
|
||||||
|
|
||||||
|
|
||||||
def test_woff2(layout_engine) -> None:
|
def test_woff2(layout_engine: ImageFont.Layout) -> None:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/OpenSans.woff2",
|
"Tests/fonts/OpenSans.woff2",
|
||||||
|
@ -1042,7 +1068,7 @@ def test_render_mono_size() -> None:
|
||||||
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
|
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
|
||||||
|
|
||||||
|
|
||||||
def test_too_many_characters(font) -> None:
|
def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
font.getlength("A" * 1_000_001)
|
font.getlength("A" * 1_000_001)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -1070,14 +1096,14 @@ def test_too_many_characters(font) -> None:
|
||||||
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
|
"Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_oom(test_file) -> None:
|
def test_oom(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
font = ImageFont.truetype(BytesIO(f.read()))
|
font = ImageFont.truetype(BytesIO(f.read()))
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
font.getmask("Test Text")
|
font.getmask("Test Text")
|
||||||
|
|
||||||
|
|
||||||
def test_raqm_missing_warning(monkeypatch) -> None:
|
def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
|
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
|
||||||
with pytest.warns(UserWarning) as record:
|
with pytest.warns(UserWarning) as record:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
|
@ -1091,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("size", [-1, 0])
|
@pytest.mark.parametrize("size", [-1, 0])
|
||||||
def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None:
|
def test_invalid_truetype_sizes_raise_valueerror(
|
||||||
|
layout_engine: ImageFont.Layout, size: int
|
||||||
|
) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
|
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
|
||||||
|
|
|
@ -208,7 +208,9 @@ def test_language() -> None:
|
||||||
),
|
),
|
||||||
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
|
ids=("None", "ltr", "rtl2", "rtl", "ttb"),
|
||||||
)
|
)
|
||||||
def test_getlength(mode, text, direction, expected) -> None:
|
def test_getlength(
|
||||||
|
mode: str, text: str, direction: str | None, expected: float
|
||||||
|
) -> None:
|
||||||
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||||
im = Image.new(mode, (1, 1), 0)
|
im = Image.new(mode, (1, 1), 0)
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
|
@ -230,7 +232,7 @@ def test_getlength(mode, text, direction, expected) -> None:
|
||||||
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
|
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
|
||||||
ids=("caron-above", "caron-below", "double-breve", "overline"),
|
ids=("caron-above", "caron-below", "double-breve", "overline"),
|
||||||
)
|
)
|
||||||
def test_getlength_combine(mode, direction, text) -> None:
|
def test_getlength_combine(mode: str, direction: str, text: str) -> None:
|
||||||
if text == "i\u0305i" and direction == "ttb":
|
if text == "i\u0305i" and direction == "ttb":
|
||||||
pytest.skip("fails with this font")
|
pytest.skip("fails with this font")
|
||||||
|
|
||||||
|
@ -250,7 +252,7 @@ def test_getlength_combine(mode, direction, text) -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
|
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
|
||||||
def test_anchor_ttb(anchor) -> None:
|
def test_anchor_ttb(anchor: str) -> None:
|
||||||
text = "f"
|
text = "f"
|
||||||
path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
|
path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png"
|
||||||
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)
|
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120)
|
||||||
|
@ -306,7 +308,9 @@ combine_tests = (
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
|
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
|
||||||
)
|
)
|
||||||
def test_combine(name, text, dir, anchor, epsilon) -> None:
|
def test_combine(
|
||||||
|
name: str, text: str, dir: str | None, anchor: str | None, epsilon: float
|
||||||
|
) -> None:
|
||||||
path = f"Tests/images/test_combine_{name}.png"
|
path = f"Tests/images/test_combine_{name}.png"
|
||||||
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
||||||
|
|
||||||
|
@ -337,7 +341,7 @@ def test_combine(name, text, dir, anchor, epsilon) -> None:
|
||||||
("rm", "right"), # pass with getsize
|
("rm", "right"), # pass with getsize
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_combine_multiline(anchor, align) -> None:
|
def test_combine_multiline(anchor: str, align: str) -> None:
|
||||||
# test that multiline text uses getlength, not getsize or getbbox
|
# test that multiline text uses getlength, not getsize or getbbox
|
||||||
|
|
||||||
path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
|
path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
|
||||||
|
|
|
@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
||||||
def test_grabclipboard_file(self) -> None:
|
def test_grabclipboard_file(self) -> None:
|
||||||
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
||||||
|
assert p.stdin is not None
|
||||||
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
|
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
|
@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
|
||||||
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
|
||||||
def test_grabclipboard_png(self) -> None:
|
def test_grabclipboard_png(self) -> None:
|
||||||
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
|
||||||
|
assert p.stdin is not None
|
||||||
p.stdin.write(
|
p.stdin.write(
|
||||||
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
|
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
|
||||||
$ms = new-object System.IO.MemoryStream(, $bytes)
|
$ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
|
@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
reason="Linux with wl-clipboard only",
|
reason="Linux with wl-clipboard only",
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
|
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
|
||||||
def test_grabclipboard_wl_clipboard(self, ext) -> None:
|
def test_grabclipboard_wl_clipboard(self, ext: str) -> None:
|
||||||
image_path = "Tests/images/hopper." + ext
|
image_path = "Tests/images/hopper." + ext
|
||||||
with open(image_path, "rb") as fp:
|
with open(image_path, "rb") as fp:
|
||||||
subprocess.call(["wl-copy"], stdin=fp)
|
subprocess.call(["wl-copy"], stdin=fp)
|
||||||
|
@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
||||||
reason="Linux with wl-clipboard only",
|
reason="Linux with wl-clipboard only",
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("arg", ("text", "--clear"))
|
@pytest.mark.parametrize("arg", ("text", "--clear"))
|
||||||
def test_grabclipboard_wl_clipboard_errors(self, arg):
|
def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None:
|
||||||
subprocess.call(["wl-copy", arg])
|
subprocess.call(["wl-copy", arg])
|
||||||
assert ImageGrab.grabclipboard() is None
|
assert ImageGrab.grabclipboard() is None
|
||||||
|
|
|
@ -10,7 +10,7 @@ from PIL import Image, ImageMorph, _imagingmorph
|
||||||
from .helper import assert_image_equal_tofile, hopper
|
from .helper import assert_image_equal_tofile, hopper
|
||||||
|
|
||||||
|
|
||||||
def string_to_img(image_string):
|
def string_to_img(image_string: str) -> Image.Image:
|
||||||
"""Turn a string image representation into a binary image"""
|
"""Turn a string image representation into a binary image"""
|
||||||
rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)]
|
rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)]
|
||||||
height = len(rows)
|
height = len(rows)
|
||||||
|
@ -38,7 +38,7 @@ A = string_to_img(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def img_to_string(im):
|
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
|
width, height = im.size
|
||||||
|
@ -48,11 +48,11 @@ def img_to_string(im):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def img_string_normalize(im):
|
def img_string_normalize(im: str) -> str:
|
||||||
return img_to_string(string_to_img(im))
|
return img_to_string(string_to_img(im))
|
||||||
|
|
||||||
|
|
||||||
def assert_img_equal_img_string(a, b_string) -> None:
|
def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None:
|
||||||
assert img_to_string(a) == img_string_normalize(b_string)
|
assert img_to_string(a) == img_string_normalize(b_string)
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ def test_str_to_img() -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
|
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
|
||||||
)
|
)
|
||||||
def test_lut(op) -> None:
|
def test_lut(op: str) -> None:
|
||||||
lb = ImageMorph.LutBuilder(op_name=op)
|
lb = ImageMorph.LutBuilder(op_name=op)
|
||||||
assert lb.get_lut() is None
|
assert lb.get_lut() is None
|
||||||
|
|
||||||
|
@ -73,15 +73,16 @@ def test_lut(op) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_no_operator_loaded() -> None:
|
def test_no_operator_loaded() -> None:
|
||||||
|
im = Image.new("L", (1, 1))
|
||||||
mop = ImageMorph.MorphOp()
|
mop = ImageMorph.MorphOp()
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
mop.apply(None)
|
mop.apply(im)
|
||||||
assert str(e.value) == "No operator loaded"
|
assert str(e.value) == "No operator loaded"
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
mop.match(None)
|
mop.match(im)
|
||||||
assert str(e.value) == "No operator loaded"
|
assert str(e.value) == "No operator loaded"
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception) as e:
|
||||||
mop.save_lut(None)
|
mop.save_lut("")
|
||||||
assert str(e.value) == "No operator loaded"
|
assert str(e.value) == "No operator loaded"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,12 @@ from .helper import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Deformer:
|
class Deformer(ImageOps.SupportsGetMesh):
|
||||||
def getmesh(self, im):
|
def getmesh(
|
||||||
|
self, im: Image.Image
|
||||||
|
) -> list[
|
||||||
|
tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
|
||||||
|
]:
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))]
|
return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))]
|
||||||
|
|
||||||
|
@ -108,7 +112,7 @@ def test_fit_same_ratio() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
|
@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
|
||||||
def test_contain(new_size) -> None:
|
def test_contain(new_size: tuple[int, int]) -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
new_im = ImageOps.contain(im, new_size)
|
new_im = ImageOps.contain(im, new_size)
|
||||||
assert new_im.size == (256, 256)
|
assert new_im.size == (256, 256)
|
||||||
|
@ -132,7 +136,7 @@ def test_contain_round() -> None:
|
||||||
("hopper.png", (256, 256)), # square
|
("hopper.png", (256, 256)), # square
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_cover(image_name, expected_size) -> None:
|
def test_cover(image_name: str, expected_size: tuple[int, int]) -> None:
|
||||||
with Image.open("Tests/images/" + image_name) as im:
|
with Image.open("Tests/images/" + image_name) as im:
|
||||||
new_im = ImageOps.cover(im, (256, 256))
|
new_im = ImageOps.cover(im, (256, 256))
|
||||||
assert new_im.size == expected_size
|
assert new_im.size == expected_size
|
||||||
|
@ -168,7 +172,7 @@ def test_pad_round() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
def test_palette(mode) -> None:
|
def test_palette(mode: str) -> None:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
|
|
||||||
# Expand
|
# Expand
|
||||||
|
@ -210,7 +214,7 @@ def test_scale() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
|
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
|
||||||
def test_expand_palette(border) -> None:
|
def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
|
||||||
with Image.open("Tests/images/p_16.tga") as im:
|
with Image.open("Tests/images/p_16.tga") as im:
|
||||||
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
|
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
|
||||||
|
|
||||||
|
@ -366,7 +370,7 @@ def test_exif_transpose() -> None:
|
||||||
for ext in exts:
|
for ext in exts:
|
||||||
with Image.open("Tests/images/hopper" + ext) as base_im:
|
with Image.open("Tests/images/hopper" + ext) as base_im:
|
||||||
|
|
||||||
def check(orientation_im) -> None:
|
def check(orientation_im: Image.Image) -> None:
|
||||||
for im in [
|
for im in [
|
||||||
orientation_im,
|
orientation_im,
|
||||||
orientation_im.copy(),
|
orientation_im.copy(),
|
||||||
|
@ -376,6 +380,7 @@ def test_exif_transpose() -> None:
|
||||||
else:
|
else:
|
||||||
original_exif = im.info["exif"]
|
original_exif = im.info["exif"]
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert transposed_im is not None
|
||||||
assert_image_similar(base_im, transposed_im, 17)
|
assert_image_similar(base_im, transposed_im, 17)
|
||||||
if orientation_im is base_im:
|
if orientation_im is base_im:
|
||||||
assert "exif" not in im.info
|
assert "exif" not in im.info
|
||||||
|
@ -387,6 +392,7 @@ def test_exif_transpose() -> None:
|
||||||
|
|
||||||
# Repeat the operation to test that it does not keep transposing
|
# Repeat the operation to test that it does not keep transposing
|
||||||
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
||||||
|
assert transposed_im2 is not None
|
||||||
assert_image_equal(transposed_im2, transposed_im)
|
assert_image_equal(transposed_im2, transposed_im)
|
||||||
|
|
||||||
check(base_im)
|
check(base_im)
|
||||||
|
@ -402,6 +408,7 @@ def test_exif_transpose() -> None:
|
||||||
assert im.getexif()[0x0112] == 3
|
assert im.getexif()[0x0112] == 3
|
||||||
|
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert transposed_im is not None
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
transposed_im._reload_exif()
|
transposed_im._reload_exif()
|
||||||
|
@ -414,12 +421,14 @@ def test_exif_transpose() -> None:
|
||||||
assert im.getexif()[0x0112] == 3
|
assert im.getexif()[0x0112] == 3
|
||||||
|
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert transposed_im is not None
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
# Orientation set directly on Image.Exif
|
# Orientation set directly on Image.Exif
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.getexif()[0x0112] = 3
|
im.getexif()[0x0112] = 3
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
|
assert transposed_im is not None
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
|
||||||
|
@ -445,7 +454,7 @@ def test_autocontrast_cutoff() -> None:
|
||||||
# Test the cutoff argument of autocontrast
|
# Test the cutoff argument of autocontrast
|
||||||
with Image.open("Tests/images/bw_gradient.png") as img:
|
with Image.open("Tests/images/bw_gradient.png") as img:
|
||||||
|
|
||||||
def autocontrast(cutoff):
|
def autocontrast(cutoff: int | tuple[int, int]):
|
||||||
return ImageOps.autocontrast(img, cutoff).histogram()
|
return ImageOps.autocontrast(img, cutoff).histogram()
|
||||||
|
|
||||||
assert autocontrast(10) == autocontrast((10, 10))
|
assert autocontrast(10) == autocontrast((10, 10))
|
||||||
|
@ -486,20 +495,20 @@ def test_autocontrast_mask_real_input() -> None:
|
||||||
assert result_nomask != result
|
assert result_nomask != result
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
ImageStat.Stat(result, mask=rect_mask).median,
|
ImageStat.Stat(result, mask=rect_mask).median,
|
||||||
[195, 202, 184],
|
(195, 202, 184),
|
||||||
threshold=2,
|
threshold=2,
|
||||||
msg="autocontrast with mask pixel incorrect",
|
msg="autocontrast with mask pixel incorrect",
|
||||||
)
|
)
|
||||||
assert_tuple_approx_equal(
|
assert_tuple_approx_equal(
|
||||||
ImageStat.Stat(result_nomask).median,
|
ImageStat.Stat(result_nomask).median,
|
||||||
[119, 106, 79],
|
(119, 106, 79),
|
||||||
threshold=2,
|
threshold=2,
|
||||||
msg="autocontrast without mask pixel incorrect",
|
msg="autocontrast without mask pixel incorrect",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_autocontrast_preserve_tone() -> None:
|
def test_autocontrast_preserve_tone() -> None:
|
||||||
def autocontrast(mode, preserve_tone):
|
def autocontrast(mode: str, preserve_tone: bool) -> list[int]:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
|
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
|
||||||
|
|
||||||
|
@ -533,7 +542,7 @@ def test_autocontrast_preserve_gradient() -> None:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
|
"color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
|
||||||
)
|
)
|
||||||
def test_autocontrast_preserve_one_color(color) -> None:
|
def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None:
|
||||||
img = Image.new("RGB", (10, 10), color)
|
img = Image.new("RGB", (10, 10), color)
|
||||||
|
|
||||||
# single color images shouldn't change
|
# single color images shouldn't change
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFilter
|
from PIL import Image, ImageFilter
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_images():
|
def test_images() -> Generator[dict[str, Image.Image], None, None]:
|
||||||
ims = {
|
ims = {
|
||||||
"im": Image.open("Tests/images/hopper.ppm"),
|
"im": Image.open("Tests/images/hopper.ppm"),
|
||||||
"snakes": Image.open("Tests/images/color_snakes.png"),
|
"snakes": Image.open("Tests/images/color_snakes.png"),
|
||||||
|
@ -18,7 +20,7 @@ def test_images():
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
|
||||||
def test_filter_api(test_images) -> None:
|
def test_filter_api(test_images: dict[str, Image.Image]) -> None:
|
||||||
im = test_images["im"]
|
im = test_images["im"]
|
||||||
|
|
||||||
test_filter = ImageFilter.GaussianBlur(2.0)
|
test_filter = ImageFilter.GaussianBlur(2.0)
|
||||||
|
@ -26,13 +28,13 @@ def test_filter_api(test_images) -> None:
|
||||||
assert i.mode == "RGB"
|
assert i.mode == "RGB"
|
||||||
assert i.size == (128, 128)
|
assert i.size == (128, 128)
|
||||||
|
|
||||||
test_filter = ImageFilter.UnsharpMask(2.0, 125, 8)
|
test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8)
|
||||||
i = im.filter(test_filter)
|
i = im.filter(test_filter2)
|
||||||
assert i.mode == "RGB"
|
assert i.mode == "RGB"
|
||||||
assert i.size == (128, 128)
|
assert i.size == (128, 128)
|
||||||
|
|
||||||
|
|
||||||
def test_usm_formats(test_images) -> None:
|
def test_usm_formats(test_images: dict[str, Image.Image]) -> None:
|
||||||
im = test_images["im"]
|
im = test_images["im"]
|
||||||
|
|
||||||
usm = ImageFilter.UnsharpMask
|
usm = ImageFilter.UnsharpMask
|
||||||
|
@ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None:
|
||||||
im.convert("YCbCr").filter(usm)
|
im.convert("YCbCr").filter(usm)
|
||||||
|
|
||||||
|
|
||||||
def test_blur_formats(test_images) -> None:
|
def test_blur_formats(test_images: dict[str, Image.Image]) -> None:
|
||||||
im = test_images["im"]
|
im = test_images["im"]
|
||||||
|
|
||||||
blur = ImageFilter.GaussianBlur
|
blur = ImageFilter.GaussianBlur
|
||||||
|
@ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None:
|
||||||
im.convert("YCbCr").filter(blur)
|
im.convert("YCbCr").filter(blur)
|
||||||
|
|
||||||
|
|
||||||
def test_usm_accuracy(test_images) -> None:
|
def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None:
|
||||||
snakes = test_images["snakes"]
|
snakes = test_images["snakes"]
|
||||||
|
|
||||||
src = snakes.convert("RGB")
|
src = snakes.convert("RGB")
|
||||||
|
@ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None:
|
||||||
assert i.tobytes() == src.tobytes()
|
assert i.tobytes() == src.tobytes()
|
||||||
|
|
||||||
|
|
||||||
def test_blur_accuracy(test_images) -> None:
|
def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None:
|
||||||
snakes = test_images["snakes"]
|
snakes = test_images["snakes"]
|
||||||
|
|
||||||
i = snakes.filter(ImageFilter.GaussianBlur(0.4))
|
i = snakes.filter(ImageFilter.GaussianBlur(0.4))
|
||||||
|
|
|
@ -67,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette() -> None:
|
||||||
(255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)),
|
(255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_getcolor_not_special(index, palette) -> None:
|
def test_getcolor_not_special(index: int, palette: ImagePalette.ImagePalette) -> None:
|
||||||
im = Image.new("P", (1, 1))
|
im = Image.new("P", (1, 1))
|
||||||
|
|
||||||
# Do not use transparency index as a new color
|
# Do not use transparency index as a new color
|
||||||
|
|