Merge branch 'main' into main
|
@ -1,3 +1,10 @@
|
|||
skip_commits:
|
||||
files:
|
||||
- ".github/**/*"
|
||||
- ".gitmodules"
|
||||
- "docs/**/*"
|
||||
- "wheels/**/*"
|
||||
|
||||
version: '{build}'
|
||||
clone_folder: c:\pillow
|
||||
init:
|
||||
|
@ -27,7 +34,7 @@ install:
|
|||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.0.0.20230317
|
||||
- choco install ghostscript --version=10.3.0
|
||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.16.5
|
||||
cibuildwheel==2.17.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
mypy==1.7.1
|
||||
mypy==1.9.0
|
||||
|
|
15
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
|
@ -48,6 +48,21 @@ Thank you.
|
|||
* Python:
|
||||
* Pillow:
|
||||
|
||||
```text
|
||||
Please paste here the output of running:
|
||||
|
||||
python3 -m PIL.report
|
||||
or
|
||||
python3 -m PIL --report
|
||||
|
||||
Or the output of the following Python code:
|
||||
|
||||
from PIL import report
|
||||
# or
|
||||
from PIL import features
|
||||
features.pilinfo(supported_formats=False)
|
||||
```
|
||||
|
||||
<!--
|
||||
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.
|
||||
|
||||
|
|
20
.github/workflows/test-cygwin.yml
vendored
|
@ -50,7 +50,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: egor-tensin/setup-cygwin@v4
|
||||
uses: cygwin/cygwin-install-action@v4
|
||||
with:
|
||||
packages: >
|
||||
gcc-g++
|
||||
|
@ -71,7 +71,6 @@ jobs:
|
|||
make
|
||||
netpbm
|
||||
perl
|
||||
python39=3.9.16-1
|
||||
python3${{ matrix.python-minor-version }}-cffi
|
||||
python3${{ matrix.python-minor-version }}-cython
|
||||
python3${{ matrix.python-minor-version }}-devel
|
||||
|
@ -89,21 +88,15 @@ jobs:
|
|||
|
||||
- name: Select Python version
|
||||
run: |
|
||||
ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3
|
||||
|
||||
- name: Get latest NumPy version
|
||||
id: latest-numpy
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
run: |
|
||||
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
|
||||
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
|
||||
|
||||
- name: Build system information
|
||||
run: |
|
||||
|
@ -113,11 +106,6 @@ jobs:
|
|||
run: |
|
||||
bash.exe .ci/install.sh
|
||||
|
||||
- name: Upgrade NumPy
|
||||
shell: dash.exe -l "{0}"
|
||||
run: |
|
||||
python3 -m pip install -U "numpy<1.26"
|
||||
|
||||
- name: Build
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
run: |
|
||||
|
|
2
.github/workflows/test-docker.yml
vendored
|
@ -43,8 +43,6 @@ jobs:
|
|||
amazon-2-amd64,
|
||||
amazon-2023-amd64,
|
||||
arch,
|
||||
centos-7-amd64,
|
||||
centos-stream-8-amd64,
|
||||
centos-stream-9-amd64,
|
||||
debian-11-bullseye-amd64,
|
||||
debian-12-bookworm-x86,
|
||||
|
|
4
.github/workflows/test-windows.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"]
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
|
@ -86,7 +86,7 @@ jobs:
|
|||
choco install nasm --no-progress
|
||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||
|
||||
choco install ghostscript --version=10.0.0.20230317 --no-progress
|
||||
choco install ghostscript --version=10.3.0 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
# Install extra test images
|
||||
|
|
38
.github/workflows/wheels-dependencies.sh
vendored
|
@ -16,26 +16,26 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=8.3.0
|
||||
LIBPNG_VERSION=1.6.40
|
||||
JPEGTURBO_VERSION=3.0.1
|
||||
HARFBUZZ_VERSION=8.4.0
|
||||
LIBPNG_VERSION=1.6.43
|
||||
JPEGTURBO_VERSION=3.0.2
|
||||
OPENJPEG_VERSION=2.5.2
|
||||
XZ_VERSION=5.4.5
|
||||
TIFF_VERSION=4.6.0
|
||||
LCMS2_VERSION=2.16
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
GIFLIB_VERSION=5.1.4
|
||||
GIFLIB_VERSION=5.2.2
|
||||
else
|
||||
GIFLIB_VERSION=5.2.1
|
||||
fi
|
||||
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
||||
ZLIB_VERSION=1.3
|
||||
ZLIB_VERSION=1.3.1
|
||||
else
|
||||
ZLIB_VERSION=1.2.8
|
||||
fi
|
||||
LIBWEBP_VERSION=1.3.2
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.16
|
||||
LIBXCB_VERSION=1.16.1
|
||||
BROTLI_VERSION=1.1.0
|
||||
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||
|
@ -62,7 +62,7 @@ function build_brotli {
|
|||
|
||||
function build {
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
export BUILD_PREFIX="/usr/local"
|
||||
sudo chown -R runner /usr/local
|
||||
fi
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
||||
|
@ -72,11 +72,11 @@ function build {
|
|||
|
||||
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
||||
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
|
||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||
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
|
||||
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
|
||||
fi
|
||||
else
|
||||
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||
|
@ -87,11 +87,6 @@ function build {
|
|||
build_tiff
|
||||
build_libpng
|
||||
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
|
||||
if [ -f /usr/local/lib64/libopenjp2.so ]; then
|
||||
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
|
||||
|
@ -131,14 +126,19 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de
|
|||
untar pillow-depends-main.zip
|
||||
|
||||
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
|
||||
# if php is installed, brew tries to reinstall these after installing openblas
|
||||
# remove cairo to fix building harfbuzz 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
|
||||
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
|
||||
fi
|
||||
|
|
3
.github/workflows/wheels-test.sh
vendored
|
@ -4,6 +4,9 @@ set -e
|
|||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
brew install fribidi
|
||||
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
|
||||
apk add curl fribidi
|
||||
else
|
||||
|
|
6
.github/workflows/wheels.yml
vendored
|
@ -5,6 +5,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
@ -14,6 +15,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
@ -99,7 +101,7 @@ jobs:
|
|||
cibw_arch: x86_64
|
||||
macosx_deployment_target: "10.10"
|
||||
- name: "macOS arm64"
|
||||
os: macos-latest
|
||||
os: macos-14
|
||||
cibw_arch: arm64
|
||||
macosx_deployment_target: "11.0"
|
||||
- name: "manylinux2014 and musllinux x86_64"
|
||||
|
@ -132,7 +134,7 @@ jobs:
|
|||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_SKIP: pp38-*
|
||||
CIBW_TEST_SKIP: "*-macosx_arm64"
|
||||
CIBW_TEST_SKIP: cp38-macosx_arm64
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.0
|
||||
rev: v0.3.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.1.1
|
||||
rev: 24.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.7
|
||||
rev: 1.7.8
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
files: ^src/
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.4
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
@ -42,6 +42,13 @@ repos:
|
|||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.28.1
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
|
@ -62,5 +69,10 @@ repos:
|
|||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
- id: check-useless-excludes
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
|
|
108
CHANGES.rst
|
@ -2,9 +2,111 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
10.3.0 (unreleased)
|
||||
10.4.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956
|
||||
[Cirras, radarhere]
|
||||
|
||||
- Support reading CMYK JPEG2000 images #7947
|
||||
[radarhere]
|
||||
|
||||
10.3.0 (2024-04-01)
|
||||
-------------------
|
||||
|
||||
- CVE-2024-28219: Use ``strncpy`` to avoid buffer overflow #7928
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Deprecate ``eval()``, replacing it with ``lambda_eval()`` and ``unsafe_eval()`` #7927
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Raise ``ValueError`` if seeking to greater than offset-sized integer in TIFF #7883
|
||||
[radarhere]
|
||||
|
||||
- Add ``--report`` argument to ``__main__.py`` to omit supported formats #7818
|
||||
[nulano, radarhere, hugovk]
|
||||
|
||||
- Added RGB to I;16, I;16L, I;16B and I;16N conversion #7918, #7920
|
||||
[radarhere]
|
||||
|
||||
- Fix editable installation with custom build backend and configuration options #7658
|
||||
[nulano, radarhere]
|
||||
|
||||
- Fix putdata() for I;16N on big-endian #7209
|
||||
[Yay295, hugovk, radarhere]
|
||||
|
||||
- Determine MPO size from markers, not EXIF data #7884
|
||||
[radarhere]
|
||||
|
||||
- Improved conversion from RGB to RGBa, LA and La #7888
|
||||
[radarhere]
|
||||
|
||||
- Support FITS images with GZIP_1 compression #7894
|
||||
[radarhere]
|
||||
|
||||
- Use I;16 mode for 9-bit JPEG 2000 images #7900
|
||||
[scaramallion, radarhere]
|
||||
|
||||
- Raise ValueError if kmeans is negative #7891
|
||||
[radarhere]
|
||||
|
||||
- Remove TIFF tag OSUBFILETYPE when saving using libtiff #7893
|
||||
[radarhere]
|
||||
|
||||
- Raise ValueError for negative values when loading P1-P3 PPM images #7882
|
||||
[radarhere]
|
||||
|
||||
- Added reading of JPEG2000 palettes #7870
|
||||
[radarhere]
|
||||
|
||||
- Added alpha_quality argument when saving WebP images #7872
|
||||
[radarhere]
|
||||
|
||||
- Fixed joined corners for ImageDraw rounded_rectangle() non-integer dimensions #7881
|
||||
[radarhere]
|
||||
|
||||
- Stop reading EPS image at EOF marker #7753
|
||||
[radarhere]
|
||||
|
||||
- PSD layer co-ordinates may be negative #7706
|
||||
[radarhere]
|
||||
|
||||
- Use subprocess with CREATE_NO_WINDOW flag in ImageShow WindowsViewer #7791
|
||||
[radarhere]
|
||||
|
||||
- When saving GIF frame that restores to background color, do not fill identical pixels #7788
|
||||
[radarhere]
|
||||
|
||||
- Fixed reading PNG iCCP compression method #7823
|
||||
[radarhere]
|
||||
|
||||
- Allow writing IFDRational to UNDEFINED tag #7840
|
||||
[radarhere]
|
||||
|
||||
- Fix logged tag name when loading Exif data #7842
|
||||
[radarhere]
|
||||
|
||||
- Use maximum frame size in IHDR chunk when saving APNG images #7821
|
||||
[radarhere]
|
||||
|
||||
- Prevent opening P TGA images without a palette #7797
|
||||
[radarhere]
|
||||
|
||||
- Use palette when loading ICO images #7798
|
||||
[radarhere]
|
||||
|
||||
- Use consistent arguments for load_read and load_seek #7713
|
||||
[radarhere]
|
||||
|
||||
- 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]
|
||||
|
||||
|
@ -4253,7 +4355,7 @@ Changelog (Pillow)
|
|||
- Documentation changes, URL update, transpose, release checklist
|
||||
[radarhere]
|
||||
|
||||
- Fixed saving to nonexistant files specified by pathlib.Path objects #1748 (fixes #1747)
|
||||
- Fixed saving to nonexistent files specified by pathlib.Path objects #1748 (fixes #1747)
|
||||
[radarhere]
|
||||
|
||||
- Round Image.crop arguments to the nearest integer #1745 (fixes #1744)
|
||||
|
@ -7464,7 +7566,7 @@ The test suite includes 400 individual tests.
|
|||
- A handbook is available (distributed separately).
|
||||
|
||||
- The coordinate system is changed so that (0,0) is now located
|
||||
in the upper left corner. This is in compliancy with ISO 12087
|
||||
in the upper left corner. This is in compliance with ISO 12087
|
||||
and 90% of all other image processing and graphics libraries.
|
||||
|
||||
- Modes "1" (bilevel) and "P" (palette) have been introduced. Note
|
||||
|
|
4
LICENSE
|
@ -1,11 +1,11 @@
|
|||
The Python Imaging Library (PIL) is
|
||||
|
||||
Copyright © 1997-2011 by Secret Labs AB
|
||||
Copyright © 1995-2011 by Fredrik Lundh
|
||||
Copyright © 1995-2011 by Fredrik Lundh and contributors
|
||||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source HPND License:
|
||||
|
||||
|
|
2
Makefile
|
@ -78,8 +78,6 @@ release-test:
|
|||
python3 selftest.py
|
||||
python3 -m pytest Tests
|
||||
python3 -m pip install .
|
||||
-rm dist/*.egg
|
||||
-rmdir dist
|
||||
python3 -m pytest -qq
|
||||
python3 -m check_manifest
|
||||
python3 -m pyroma .
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
## Python Imaging Library (Fork)
|
||||
|
||||
Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and
|
||||
Pillow is the friendly PIL fork by [Jeffrey A. Clark and
|
||||
contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
|
||||
PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
|
||||
PIL is the Python Imaging Library by Fredrik Lundh and contributors.
|
||||
As of 2019, Pillow development is
|
||||
[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
|
||||
|
||||
|
@ -101,7 +101,7 @@ The core image library is designed for fast access to data stored in a few basic
|
|||
## More Information
|
||||
|
||||
- [Documentation](https://pillow.readthedocs.io/)
|
||||
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
|
||||
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
|
||||
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
|
||||
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
|
||||
- [Issues](https://github.com/python-pillow/Pillow/issues)
|
||||
|
|
20
RELEASING.md
|
@ -20,8 +20,10 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
git tag 5.2.0
|
||||
git push --tags
|
||||
```
|
||||
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
|
||||
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
|
||||
```bash
|
||||
|
@ -50,7 +52,9 @@ Released as needed for security, installation or critical bug fixes.
|
|||
```bash
|
||||
make sdist
|
||||
```
|
||||
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||
```bash
|
||||
git push
|
||||
|
@ -72,18 +76,14 @@ Released as needed privately to individual vendors for critical security-related
|
|||
git tag 2.5.3
|
||||
git push origin --tags
|
||||
```
|
||||
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||
```bash
|
||||
git push origin 2.5.x
|
||||
```
|
||||
|
||||
## Source and Binary Distributions
|
||||
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
|
||||
## Publicize Release
|
||||
|
||||
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
|
||||
|
|
|
@ -32,9 +32,8 @@ def timer(func, label, *args) -> None:
|
|||
break
|
||||
endtime = time.time()
|
||||
print(
|
||||
"{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format(
|
||||
label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0)
|
||||
)
|
||||
f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
|
||||
f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -23,7 +23,10 @@ def _get_mem_usage() -> float:
|
|||
|
||||
|
||||
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:
|
||||
mem_limit = None
|
||||
for i in range(max_iterations):
|
||||
|
|
|
@ -17,6 +17,7 @@ def test_ignore_dos_text() -> None:
|
|||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
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"
|
||||
return
|
||||
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
|
||||
|
@ -57,6 +59,7 @@ def test_dos_total_memory() -> None:
|
|||
return
|
||||
|
||||
total_len = 0
|
||||
assert isinstance(im2, PngImagePlugin.PngImageFile)
|
||||
for txt in im2.text.values():
|
||||
total_len += len(txt)
|
||||
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"
|
||||
|
|
|
@ -11,6 +11,7 @@ import subprocess
|
|||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
from io import BytesIO
|
||||
from typing import Any, Callable, Sequence
|
||||
|
||||
|
@ -114,7 +115,9 @@ def assert_image_similar(
|
|||
|
||||
diff = 0
|
||||
for ach, bch in zip(a.split(), b.split()):
|
||||
chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L")
|
||||
chdiff = ImageMath.lambda_eval(
|
||||
lambda args: abs(args["a"] - args["b"]), a=ach, b=bch
|
||||
).convert("L")
|
||||
diff += sum(i * num for i, num in enumerate(chdiff.histogram()))
|
||||
|
||||
ave_diff = diff / (a.size[0] * a.size[1])
|
||||
|
@ -250,25 +253,27 @@ def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
|
|||
return out.getvalue()
|
||||
|
||||
|
||||
def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
|
||||
def hopper(mode: str | None = None) -> Image.Image:
|
||||
# Use caching to reduce reading from disk, but return a copy
|
||||
# so that the cached image isn't modified by the tests
|
||||
# (for fast, isolated, repeatable tests).
|
||||
|
||||
if mode is None:
|
||||
# Always return fresh not-yet-loaded version of image.
|
||||
# Operations on not-yet-loaded images is separate class of errors
|
||||
# what we should catch.
|
||||
# Operations on not-yet-loaded images are a separate class of errors
|
||||
# that we should catch.
|
||||
return Image.open("Tests/images/hopper.ppm")
|
||||
# Use caching to reduce reading from disk but so an original copy is
|
||||
# returned each time and the cached image isn't modified by tests
|
||||
# (for fast, isolated, repeatable tests).
|
||||
im = cache.get(mode)
|
||||
if im is None:
|
||||
if mode == "F":
|
||||
im = hopper("L").convert(mode)
|
||||
elif mode[:4] == "I;16":
|
||||
im = hopper("I").convert(mode)
|
||||
else:
|
||||
im = hopper().convert(mode)
|
||||
cache[mode] = im
|
||||
return im.copy()
|
||||
|
||||
return _cached_hopper(mode).copy()
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _cached_hopper(mode: str) -> Image.Image:
|
||||
if mode == "F":
|
||||
im = hopper("L")
|
||||
else:
|
||||
im = hopper()
|
||||
return im.convert(mode)
|
||||
|
||||
|
||||
def djpeg_available() -> bool:
|
||||
|
@ -351,7 +356,7 @@ def is_mingw() -> bool:
|
|||
|
||||
|
||||
class CachedProperty:
|
||||
def __init__(self, func: Callable[[Any], None]) -> None:
|
||||
def __init__(self, func: Callable[[Any], Any]) -> None:
|
||||
self.func = func
|
||||
|
||||
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
|
||||
|
|
BIN
Tests/icc/sGrey-v2-nano.icc
Normal file
Before Width: | Height: | Size: 578 B |
BIN
Tests/images/16_bit_binary_pgm.tiff
Normal file
BIN
Tests/images/9bit.j2k
Normal file
BIN
Tests/images/bmp/q/rgb32h52.bmp
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
Tests/images/bmp/q/rgba32h56.bmp
Normal file
After Width: | Height: | Size: 32 KiB |
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/m13.fits
Normal file
366
Tests/images/m13_gzip.fits
Normal file
BIN
Tests/images/negative_top_left_layer.psd
Normal file
BIN
Tests/images/p_8.tga
Normal file
BIN
Tests/images/seek_too_large.tif
Normal file
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
BIN
Tests/images/unknown_compression_method.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
|
@ -7,7 +7,7 @@ import fuzzers
|
|||
import packaging
|
||||
import pytest
|
||||
|
||||
from PIL import Image, features
|
||||
from PIL import Image, UnidentifiedImageError, features
|
||||
from Tests.helper import skip_unless_feature
|
||||
|
||||
if sys.platform.startswith("win32"):
|
||||
|
@ -43,7 +43,7 @@ def test_fuzz_images(path: str) -> None:
|
|||
except (
|
||||
Image.DecompressionBombError,
|
||||
Image.DecompressionBombWarning,
|
||||
Image.UnidentifiedImageError,
|
||||
UnidentifiedImageError,
|
||||
):
|
||||
# Known Image.* exceptions
|
||||
assert True
|
||||
|
|
|
@ -44,6 +44,9 @@ def test_questionable() -> None:
|
|||
"pal8os2sp.bmp",
|
||||
"pal8rletrns.bmp",
|
||||
"rgb32bf-xbgr.bmp",
|
||||
"rgba32.bmp",
|
||||
"rgb32h52.bmp",
|
||||
"rgba32h56.bmp",
|
||||
]
|
||||
for f in get_files("q"):
|
||||
try:
|
||||
|
|
|
@ -117,9 +117,10 @@ def test_unsupported_module() -> None:
|
|||
features.version_module(module)
|
||||
|
||||
|
||||
def test_pilinfo() -> None:
|
||||
@pytest.mark.parametrize("supported_formats", (True, False))
|
||||
def test_pilinfo(supported_formats) -> None:
|
||||
buf = io.StringIO()
|
||||
features.pilinfo(buf)
|
||||
features.pilinfo(buf, supported_formats=supported_formats)
|
||||
out = buf.getvalue()
|
||||
lines = out.splitlines()
|
||||
assert lines[0] == "-" * 68
|
||||
|
@ -129,9 +130,15 @@ def test_pilinfo() -> None:
|
|||
while lines[0].startswith(" "):
|
||||
lines = lines[1:]
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Python modules loaded from ")
|
||||
assert lines[2].startswith("Binary modules loaded from ")
|
||||
assert lines[3] == "-" * 68
|
||||
assert lines[1].startswith("Python executable is")
|
||||
lines = lines[2:]
|
||||
if lines[0].startswith("Environment Python files loaded from"):
|
||||
lines = lines[1:]
|
||||
assert lines[0].startswith("System Python files loaded from")
|
||||
assert lines[1] == "-" * 68
|
||||
assert lines[2].startswith("Python Pillow modules loaded from ")
|
||||
assert lines[3].startswith("Binary Pillow modules loaded from ")
|
||||
assert lines[4] == "-" * 68
|
||||
jpeg = (
|
||||
"\n"
|
||||
+ "-" * 68
|
||||
|
@ -142,4 +149,4 @@ def test_pilinfo() -> None:
|
|||
+ "-" * 68
|
||||
+ "\n"
|
||||
)
|
||||
assert jpeg in out
|
||||
assert supported_formats == (jpeg in out)
|
||||
|
|
|
@ -668,6 +668,16 @@ def test_apng_save_blend(tmp_path: Path) -> None:
|
|||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
||||
|
||||
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:
|
||||
im = Image.open("Tests/images/apng/delay.png")
|
||||
im.seek(1)
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import BmpImagePlugin, Image
|
||||
from PIL import BmpImagePlugin, Image, _binary
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -128,6 +128,29 @@ def test_load_dib() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"header_size, path",
|
||||
(
|
||||
(12, "g/pal8os2.bmp"),
|
||||
(40, "g/pal1.bmp"),
|
||||
(52, "q/rgb32h52.bmp"),
|
||||
(56, "q/rgba32h56.bmp"),
|
||||
(64, "q/pal8os2v2.bmp"),
|
||||
(108, "g/pal8v4.bmp"),
|
||||
(124, "g/pal8v5.bmp"),
|
||||
),
|
||||
)
|
||||
def test_dib_header_size(header_size, path):
|
||||
image_path = "Tests/images/bmp/" + path
|
||||
with open(image_path, "rb") as fp:
|
||||
data = fp.read()[14:]
|
||||
assert _binary.i32le(data) == header_size
|
||||
|
||||
dib = io.BytesIO(data)
|
||||
with Image.open(dib) as im:
|
||||
im.load()
|
||||
|
||||
|
||||
def test_save_dib(tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.dib")
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import ContainerIO, Image
|
||||
|
@ -21,9 +23,16 @@ def test_isatty() -> None:
|
|||
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
|
||||
mode = 0
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
|
@ -32,35 +41,7 @@ def test_seek_mode_0() -> None:
|
|||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 33
|
||||
|
||||
|
||||
def test_seek_mode_1() -> None:
|
||||
# Arrange
|
||||
mode = 1
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(33, mode)
|
||||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 66
|
||||
|
||||
|
||||
def test_seek_mode_2() -> None:
|
||||
# Arrange
|
||||
mode = 2
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(33, mode)
|
||||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 100
|
||||
assert container.tell() == expected_position
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import EpsImagePlugin, Image, features
|
||||
from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_similar,
|
||||
|
@ -419,7 +419,7 @@ def test_emptyline() -> None:
|
|||
)
|
||||
def test_timeout(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(Image.UnidentifiedImageError):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
@ -437,3 +437,11 @@ def test_eof_before_bounding_box() -> None:
|
|||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||
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"
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
|
||||
from PIL import FitsImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.fits"
|
||||
|
||||
|
@ -22,6 +22,11 @@ def test_open() -> None:
|
|||
assert_image_equal(im, hopper("L"))
|
||||
|
||||
|
||||
def test_gzip1() -> None:
|
||||
with Image.open("Tests/images/m13_gzip.fits") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/m13.fits")
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
|
|
@ -647,6 +647,9 @@ def test_dispose2_palette(tmp_path: Path) -> None:
|
|||
# Center remains red every frame
|
||||
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:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
@ -734,6 +737,25 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
|
|||
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:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||
|
|
|
@ -38,6 +38,17 @@ def test_black_and_white() -> None:
|
|||
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:
|
||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||
with pytest.raises(SyntaxError):
|
||||
|
|
|
@ -6,7 +6,7 @@ import warnings
|
|||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -45,14 +45,20 @@ TEST_FILE = "Tests/images/hopper.jpg"
|
|||
|
||||
@skip_unless_feature("jpg")
|
||||
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()
|
||||
im.save(out, "JPEG", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
im = Image.open(out)
|
||||
im.bytes = test_bytes # for testing only
|
||||
return im
|
||||
reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out))
|
||||
return reloaded, test_bytes
|
||||
|
||||
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:
|
||||
"""Generates a very hard to compress file
|
||||
|
@ -246,13 +252,13 @@ class TestFileJpeg:
|
|||
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
|
||||
|
||||
def test_optimize(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im2 = self.roundtrip(hopper(), optimize=0)
|
||||
im3 = self.roundtrip(hopper(), optimize=1)
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0)
|
||||
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1)
|
||||
assert_image_equal(im1, im2)
|
||||
assert_image_equal(im1, im3)
|
||||
assert im1.bytes >= im2.bytes
|
||||
assert im1.bytes >= im3.bytes
|
||||
assert im1_bytes >= im2_bytes
|
||||
assert im1_bytes >= im3_bytes
|
||||
|
||||
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
|
||||
# https://github.com/python-pillow/Pillow/issues/148
|
||||
|
@ -262,15 +268,15 @@ class TestFileJpeg:
|
|||
im.save(f, format="JPEG", optimize=True)
|
||||
|
||||
def test_progressive(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
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 im2.info.get("progressive")
|
||||
assert im3.info.get("progressive")
|
||||
|
||||
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:
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
|
@ -341,6 +347,7 @@ class TestFileJpeg:
|
|||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
transposed = ImageOps.exif_transpose(im)
|
||||
assert transposed is not None
|
||||
exif = transposed.getexif()
|
||||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
|
@ -419,14 +426,14 @@ class TestFileJpeg:
|
|||
assert im3.info.get("progression")
|
||||
|
||||
def test_quality(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im2 = self.roundtrip(hopper(), quality=50)
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50)
|
||||
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 im2.bytes > im3.bytes
|
||||
assert im2_bytes > im3_bytes
|
||||
|
||||
def test_smooth(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
|
@ -818,7 +825,7 @@ class TestFileJpeg:
|
|||
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
|
||||
# Act / Assert
|
||||
# "When the image resolution is unknown, 72 [dpi] is designated."
|
||||
# https://exiv2.org/tags.html
|
||||
# https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_invalid_exif(self) -> None:
|
||||
|
@ -986,13 +993,7 @@ class TestFileJpeg:
|
|||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
return 0, 0
|
||||
|
||||
decoder = InfiniteMockPyDecoder(None)
|
||||
|
||||
def closure(mode: str, *args) -> InfiniteMockPyDecoder:
|
||||
decoder.__init__(mode, *args)
|
||||
return decoder
|
||||
|
||||
Image.register_decoder("INFINITE", closure)
|
||||
Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.tile = [
|
||||
|
|
|
@ -40,10 +40,8 @@ test_card.load()
|
|||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
out = BytesIO()
|
||||
im.save(out, "JPEG2000", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
with Image.open(out) as im:
|
||||
im.bytes = test_bytes # for testing only
|
||||
im.load()
|
||||
return im
|
||||
|
||||
|
@ -77,7 +75,9 @@ def test_invalid_file() -> None:
|
|||
def test_bytesio() -> None:
|
||||
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
|
||||
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
|
||||
|
@ -289,6 +289,16 @@ def test_rgba(ext: str) -> None:
|
|||
assert im.mode == "RGBA"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
@skip_unless_feature_version("jpg_2000", "2.5.1")
|
||||
def test_cmyk() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im:
|
||||
assert im.mode == "CMYK"
|
||||
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
|
||||
with Image.open("Tests/images/16bit.cropped" + ext) as im:
|
||||
|
@ -340,6 +350,7 @@ def test_parser_feed() -> None:
|
|||
p.feed(data)
|
||||
|
||||
# Assert
|
||||
assert p.image is not None
|
||||
assert p.image.size == (640, 480)
|
||||
|
||||
|
||||
|
@ -363,6 +374,16 @@ def test_subsampling_decode(name: str) -> None:
|
|||
assert_image_similar(im, expected, epsilon)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
def test_pclr() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
|
||||
assert im.mode == "P"
|
||||
assert len(im.palette.colors) == 256
|
||||
assert im.palette.colors[(255, 255, 255)] == 0
|
||||
|
||||
|
||||
def test_comment() -> None:
|
||||
with Image.open("Tests/images/comment.jp2") as im:
|
||||
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||
|
@ -435,3 +456,9 @@ def test_plt_marker() -> None:
|
|||
hdr = out.read(2)
|
||||
length = _binary.i16be(hdr)
|
||||
out.seek(length - 2, os.SEEK_CUR)
|
||||
|
||||
|
||||
def test_9bit():
|
||||
with Image.open("Tests/images/9bit.j2k") as im:
|
||||
assert im.mode == "I;16"
|
||||
assert im.size == (128, 128)
|
||||
|
|
|
@ -6,13 +6,13 @@ import itertools
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
|
||||
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -27,7 +27,7 @@ from .helper import (
|
|||
|
||||
@skip_unless_feature("libtiff")
|
||||
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"""
|
||||
# 1 bit
|
||||
assert im.mode == "1"
|
||||
|
@ -243,36 +243,40 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
def test_custom_metadata(self, tmp_path: Path) -> None:
|
||||
tc = namedtuple("tc", "value,type,supported_by_default")
|
||||
class Tc(NamedTuple):
|
||||
value: Any
|
||||
type: int
|
||||
supported_by_default: bool
|
||||
|
||||
custom = {
|
||||
37000 + k: v
|
||||
for k, v in enumerate(
|
||||
[
|
||||
tc(4, TiffTags.SHORT, True),
|
||||
tc(123456789, TiffTags.LONG, True),
|
||||
tc(-4, TiffTags.SIGNED_BYTE, False),
|
||||
tc(-4, TiffTags.SIGNED_SHORT, False),
|
||||
tc(-123456789, TiffTags.SIGNED_LONG, False),
|
||||
tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
|
||||
tc(4.25, TiffTags.FLOAT, True),
|
||||
tc(4.25, TiffTags.DOUBLE, True),
|
||||
tc("custom tag value", TiffTags.ASCII, True),
|
||||
tc(b"custom tag value", TiffTags.BYTE, True),
|
||||
tc((4, 5, 6), TiffTags.SHORT, True),
|
||||
tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
|
||||
tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
|
||||
tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
|
||||
tc(
|
||||
Tc(4, TiffTags.SHORT, True),
|
||||
Tc(123456789, TiffTags.LONG, True),
|
||||
Tc(-4, TiffTags.SIGNED_BYTE, False),
|
||||
Tc(-4, TiffTags.SIGNED_SHORT, False),
|
||||
Tc(-123456789, TiffTags.SIGNED_LONG, False),
|
||||
Tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
|
||||
Tc(4.25, TiffTags.FLOAT, True),
|
||||
Tc(4.25, TiffTags.DOUBLE, True),
|
||||
Tc("custom tag value", TiffTags.ASCII, True),
|
||||
Tc(b"custom tag value", TiffTags.BYTE, True),
|
||||
Tc((4, 5, 6), TiffTags.SHORT, True),
|
||||
Tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
|
||||
Tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
|
||||
Tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
|
||||
Tc(
|
||||
(-123456789, 9, 34, 234, 219387, -92432323),
|
||||
TiffTags.SIGNED_LONG,
|
||||
False,
|
||||
),
|
||||
tc((4.25, 5.25), TiffTags.FLOAT, True),
|
||||
tc((4.25, 5.25), TiffTags.DOUBLE, True),
|
||||
Tc((4.25, 5.25), TiffTags.FLOAT, True),
|
||||
Tc((4.25, 5.25), TiffTags.DOUBLE, True),
|
||||
# array of TIFF_BYTE requires bytes instead of tuple for backwards
|
||||
# compatibility
|
||||
tc(bytes([4]), TiffTags.BYTE, True),
|
||||
tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
|
||||
Tc(bytes([4]), TiffTags.BYTE, True),
|
||||
Tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
@ -325,6 +329,12 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
)
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
def test_osubfiletype(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||
im.tag_v2[OSUBFILETYPE] = 1
|
||||
im.save(outfile)
|
||||
|
||||
def test_subifd(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||
|
@ -524,7 +534,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, compression=compression)
|
||||
|
||||
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()
|
||||
|
||||
os.fstat(fn)
|
||||
|
@ -716,6 +727,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
f.write(src.read())
|
||||
|
||||
im = Image.open(tmpfile)
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.n_frames
|
||||
im.close()
|
||||
# Should not raise PermissionError.
|
||||
|
@ -1097,6 +1109,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
with Image.open(out) as im:
|
||||
# Assert that there are multiple strips
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
||||
|
||||
@pytest.mark.parametrize("argument", (True, False))
|
||||
|
@ -1113,6 +1126,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, **arguments)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
||||
finally:
|
||||
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/pub/fieldCampaigns/camex3/cmx3g8/browse/
|
||||
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
|
||||
with Image.open(test_file) as im:
|
||||
|
|
|
@ -2,11 +2,11 @@ from __future__ import annotations
|
|||
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, MpoImagePlugin
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -20,14 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
|||
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()
|
||||
im.save(out, "MPO", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
im = Image.open(out)
|
||||
im.bytes = test_bytes # for testing only
|
||||
return im
|
||||
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
|
@ -96,7 +93,7 @@ def test_exif(test_file: str) -> None:
|
|||
|
||||
def test_frame_size() -> None:
|
||||
# This image has been hexedited to contain a different size
|
||||
# in the EXIF data of the second frame
|
||||
# in the SOF marker of the second frame
|
||||
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
|
||||
assert im.size == (640, 480)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import zlib
|
|||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -59,11 +59,11 @@ def load(data: bytes) -> Image.Image:
|
|||
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()
|
||||
im.save(out, "PNG", **options)
|
||||
out.seek(0)
|
||||
return Image.open(out)
|
||||
return cast(PngImagePlugin.PngImageFile, Image.open(out))
|
||||
|
||||
|
||||
@skip_unless_feature("zlib")
|
||||
|
@ -102,7 +102,7 @@ class TestFilePng:
|
|||
im = hopper(mode)
|
||||
im.save(test_file)
|
||||
with Image.open(test_file) as reloaded:
|
||||
if mode in ("I;16", "I;16B"):
|
||||
if mode in ("I", "I;16B"):
|
||||
reloaded = reloaded.convert(mode)
|
||||
assert_image_equal(reloaded, im)
|
||||
|
||||
|
@ -304,8 +304,8 @@ class TestFilePng:
|
|||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||
|
||||
def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
|
||||
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
|
||||
in_file = "Tests/images/" + mode.lower() + "_trns.png"
|
||||
for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items():
|
||||
in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png"
|
||||
with Image.open(in_file) as im:
|
||||
assert im.mode == mode
|
||||
assert im.info["transparency"] == 255
|
||||
|
@ -619,6 +619,10 @@ class TestFilePng:
|
|||
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
||||
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:
|
||||
# This image has been manually hexedited
|
||||
# so that the IDAT chunk has padding at the end
|
||||
|
|
|
@ -88,7 +88,7 @@ def test_16bit_pgm() -> None:
|
|||
assert im.size == (20, 100)
|
||||
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:
|
||||
|
@ -241,13 +241,23 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
|
|||
im.load()
|
||||
|
||||
|
||||
def test_plain_ppm_value_negative(tmp_path: Path) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P3\n128 128\n255\n-1")
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError, match="Channel value is negative"):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P3\n128 128\n255\n256")
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Channel value too large"):
|
||||
im.load()
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, PsdImagePlugin
|
||||
from PIL import Image, PsdImagePlugin, UnidentifiedImageError
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
||||
|
||||
|
@ -113,6 +113,11 @@ def test_rgba() -> None:
|
|||
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:
|
||||
with Image.open("Tests/images/five_channels.psd") as im:
|
||||
assert im.n_frames == 1
|
||||
|
@ -147,11 +152,11 @@ def test_combined_larger_than_size() -> None:
|
|||
[
|
||||
(
|
||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||
Image.UnidentifiedImageError,
|
||||
UnidentifiedImageError,
|
||||
),
|
||||
(
|
||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||
Image.UnidentifiedImageError,
|
||||
UnidentifiedImageError,
|
||||
),
|
||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||
|
|
|
@ -9,7 +9,7 @@ import pytest
|
|||
|
||||
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"
|
||||
|
||||
|
@ -160,4 +160,5 @@ def test_odd_size() -> None:
|
|||
im.save(data, format="SPIDER")
|
||||
|
||||
data.seek(0)
|
||||
assert_image_equal_tofile(im, data)
|
||||
with Image.open(data) as im2:
|
||||
assert_image_equal(im, im2)
|
||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
@ -65,6 +65,11 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
|
|||
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:
|
||||
with Image.open("Tests/images/p_16.tga") as im:
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
im = hopper("PA")
|
||||
out = str(tmp_path / "temp.tga")
|
||||
|
|
|
@ -113,6 +113,10 @@ class TestFileTiff:
|
|||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||
|
||||
def test_seek_too_large(self):
|
||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||
Image.open("Tests/images/seek_too_large.tif")
|
||||
|
||||
def test_set_legacy_api(self) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
with pytest.raises(Exception) as e:
|
||||
|
@ -623,6 +627,7 @@ class TestFileTiff:
|
|||
im.save(outfile, tiffinfo={278: 256})
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2[278] == 256
|
||||
|
||||
def test_strip_raw(self) -> None:
|
||||
|
|
|
@ -224,14 +224,17 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path)
|
|||
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()
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
tag = TiffTags.TAGS_V2[33723]
|
||||
assert tag.type == TiffTags.UNDEFINED
|
||||
|
||||
info[33723] = 1
|
||||
info[33723] = value
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
im.save(out, tiffinfo=info)
|
||||
|
|
|
@ -151,3 +151,15 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
|
|||
target = im.convert("RGBA")
|
||||
|
||||
assert_image_similar(image, target, 25.0)
|
||||
|
||||
|
||||
def test_alpha_quality(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
out = str(tmp_path / "temp.webp")
|
||||
im.save(out)
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
im.save(out_quality, alpha_quality=50)
|
||||
with Image.open(out) as reloaded:
|
||||
with Image.open(out_quality) as reloaded_quality:
|
||||
assert reloaded.tobytes() != reloaded_quality.tobytes()
|
||||
|
|
|
@ -188,3 +188,21 @@ def test_seek_errors() -> None:
|
|||
|
||||
with pytest.raises(EOFError):
|
||||
im.seek(42)
|
||||
|
||||
|
||||
def test_alpha_quality(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
first_frame = Image.new("L", im.size)
|
||||
|
||||
out = str(tmp_path / "temp.webp")
|
||||
first_frame.save(out, save_all=True, append_images=[im])
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
first_frame.save(
|
||||
out_quality, save_all=True, append_images=[im], alpha_quality=50
|
||||
)
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded.seek(1)
|
||||
with Image.open(out_quality) as reloaded_quality:
|
||||
reloaded_quality.seek(1)
|
||||
assert reloaded.tobytes() != reloaded_quality.tobytes()
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
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):
|
||||
|
@ -31,5 +33,11 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
|
|||
mem_limit = 1024 # k
|
||||
|
||||
def test_leak(self) -> None:
|
||||
default_font = ImageFont.load_default()
|
||||
if features.check_module("freetype2"):
|
||||
ImageFont.core = _util.DeferredError(ImportError)
|
||||
try:
|
||||
default_font = ImageFont.load_default()
|
||||
finally:
|
||||
ImageFont.core = original_core
|
||||
|
||||
self._test_font(default_font)
|
||||
|
|
|
@ -16,6 +16,7 @@ from PIL import (
|
|||
ExifTags,
|
||||
Image,
|
||||
ImageDraw,
|
||||
ImageFile,
|
||||
ImagePalette,
|
||||
UnidentifiedImageError,
|
||||
features,
|
||||
|
@ -32,36 +33,38 @@ from .helper import (
|
|||
skip_unless_feature,
|
||||
)
|
||||
|
||||
# name, pixel size
|
||||
image_modes = (
|
||||
("1", 1),
|
||||
("L", 1),
|
||||
("LA", 4),
|
||||
("La", 4),
|
||||
("P", 1),
|
||||
("PA", 4),
|
||||
("F", 4),
|
||||
("I", 4),
|
||||
("I;16", 2),
|
||||
("I;16L", 2),
|
||||
("I;16B", 2),
|
||||
("I;16N", 2),
|
||||
("RGB", 4),
|
||||
("RGBA", 4),
|
||||
("RGBa", 4),
|
||||
("RGBX", 4),
|
||||
("BGR;15", 2),
|
||||
("BGR;16", 2),
|
||||
("BGR;24", 3),
|
||||
("CMYK", 4),
|
||||
("YCbCr", 4),
|
||||
("HSV", 4),
|
||||
("LAB", 4),
|
||||
)
|
||||
|
||||
image_mode_names = [name for name, _ in image_modes]
|
||||
|
||||
|
||||
class TestImage:
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
(
|
||||
"1",
|
||||
"P",
|
||||
"PA",
|
||||
"L",
|
||||
"LA",
|
||||
"La",
|
||||
"F",
|
||||
"I",
|
||||
"I;16",
|
||||
"I;16L",
|
||||
"I;16B",
|
||||
"I;16N",
|
||||
"RGB",
|
||||
"RGBX",
|
||||
"RGBA",
|
||||
"RGBa",
|
||||
"BGR;15",
|
||||
"BGR;16",
|
||||
"BGR;24",
|
||||
"CMYK",
|
||||
"YCbCr",
|
||||
"LAB",
|
||||
"HSV",
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("mode", image_mode_names)
|
||||
def test_image_modes_success(self, mode: str) -> None:
|
||||
Image.new(mode, (1, 1))
|
||||
|
||||
|
@ -138,13 +141,13 @@ class TestImage:
|
|||
assert im.height == 2
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
im.size = (3, 4)
|
||||
im.size = (3, 4) # type: ignore[misc]
|
||||
|
||||
def test_set_mode(self) -> None:
|
||||
im = Image.new("RGB", (1, 1))
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
im.mode = "P"
|
||||
im.mode = "P" # type: ignore[misc]
|
||||
|
||||
def test_invalid_image(self) -> None:
|
||||
im = io.BytesIO(b"")
|
||||
|
@ -1041,25 +1044,49 @@ class TestImage:
|
|||
assert im.fp is None
|
||||
|
||||
|
||||
class MockEncoder:
|
||||
args: tuple[str, ...]
|
||||
class TestImageBytes:
|
||||
@pytest.mark.parametrize("mode", image_mode_names)
|
||||
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
source_bytes = im.tobytes()
|
||||
|
||||
reloaded = Image.frombytes(mode, im.size, source_bytes)
|
||||
assert reloaded.tobytes() == source_bytes
|
||||
|
||||
@pytest.mark.parametrize("mode", image_mode_names)
|
||||
def test_roundtrip_bytes_method(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
source_bytes = im.tobytes()
|
||||
|
||||
reloaded = Image.new(mode, im.size)
|
||||
reloaded.frombytes(source_bytes)
|
||||
assert reloaded.tobytes() == source_bytes
|
||||
|
||||
@pytest.mark.parametrize(("mode", "pixelsize"), image_modes)
|
||||
def test_getdata_putdata(self, mode: str, pixelsize: int) -> None:
|
||||
im = Image.new(mode, (2, 2))
|
||||
source_bytes = bytes(range(im.width * im.height * pixelsize))
|
||||
im.frombytes(source_bytes)
|
||||
|
||||
reloaded = Image.new(mode, im.size)
|
||||
reloaded.putdata(im.getdata())
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
|
||||
def mock_encode(*args: str) -> MockEncoder:
|
||||
encoder = MockEncoder()
|
||||
encoder.args = args
|
||||
return encoder
|
||||
class MockEncoder(ImageFile.PyEncoder):
|
||||
pass
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
def test_encode_registry(self) -> None:
|
||||
Image.register_encoder("MOCK", mock_encode)
|
||||
Image.register_encoder("MOCK", MockEncoder)
|
||||
assert "MOCK" in Image.ENCODERS
|
||||
|
||||
enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",))
|
||||
|
||||
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:
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -14,6 +14,7 @@ from .helper import assert_image_equal, hopper, is_win32
|
|||
|
||||
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
|
||||
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
|
||||
cffi: ModuleType | None
|
||||
if os.environ.get("PYTHONOPTIMIZE") == "2":
|
||||
cffi = None
|
||||
else:
|
||||
|
|
|
@ -91,6 +91,16 @@ def test_fromarray() -> None:
|
|||
Image.fromarray(wrapped)
|
||||
|
||||
|
||||
def test_fromarray_strides_without_tobytes() -> None:
|
||||
class Wrapper:
|
||||
def __init__(self, arr_params: dict[str, Any]) -> None:
|
||||
self.__array_interface__ = arr_params
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
|
||||
Image.fromarray(wrapped, "L")
|
||||
|
||||
|
||||
def test_fromarray_palette() -> None:
|
||||
# Arrange
|
||||
i = im.convert("L")
|
||||
|
|
|
@ -183,6 +183,14 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
|||
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
|
||||
im_l.save(f)
|
||||
|
||||
im_la = im.convert("LA")
|
||||
assert "transparency" not in im_la.info
|
||||
im_la.save(f)
|
||||
|
||||
im_la = im.convert("La")
|
||||
assert "transparency" not in im_la.info
|
||||
assert im_la.getpixel((0, 0)) == (0, 0)
|
||||
|
||||
im_p = im.convert("P")
|
||||
assert "transparency" in im_p.info
|
||||
im_p.save(f)
|
||||
|
@ -191,6 +199,10 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
|||
assert "transparency" not in im_rgba.info
|
||||
im_rgba.save(f)
|
||||
|
||||
im_rgba = im.convert("RGBa")
|
||||
assert "transparency" not in im_rgba.info
|
||||
assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
|
||||
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE)
|
||||
assert "transparency" not in im_p.info
|
||||
im_p.save(f)
|
||||
|
|
|
@ -148,9 +148,7 @@ def test_kernel_not_enough_coefficients() -> None:
|
|||
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
||||
def test_consistency_3x3(mode: str) -> None:
|
||||
with Image.open("Tests/images/hopper.bmp") as source:
|
||||
reference_name = "hopper_emboss"
|
||||
reference_name += "_I.png" if mode == "I" else ".bmp"
|
||||
with Image.open("Tests/images/" + reference_name) as reference:
|
||||
with Image.open("Tests/images/hopper_emboss.bmp") as reference:
|
||||
kernel = ImageFilter.Kernel(
|
||||
(3, 3),
|
||||
# fmt: off
|
||||
|
@ -160,23 +158,13 @@ def test_consistency_3x3(mode: str) -> None:
|
|||
# fmt: on
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
|
||||
def test_consistency_5x5(mode: str) -> None:
|
||||
with Image.open("Tests/images/hopper.bmp") as source:
|
||||
reference_name = "hopper_emboss_more"
|
||||
reference_name += "_I.png" if mode == "I" else ".bmp"
|
||||
with Image.open("Tests/images/" + reference_name) as reference:
|
||||
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
|
||||
kernel = ImageFilter.Kernel(
|
||||
(5, 5),
|
||||
# fmt: off
|
||||
|
@ -188,14 +176,6 @@ def test_consistency_5x5(mode: str) -> None:
|
|||
# fmt: on
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -17,19 +16,18 @@ pytestmark = pytest.mark.skipif(
|
|||
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
||||
)
|
||||
|
||||
ims: list[Image.Image] = []
|
||||
|
||||
@pytest.fixture
|
||||
def test_images() -> Generator[Image.Image, None, None]:
|
||||
ims = [
|
||||
hopper(),
|
||||
Image.open("Tests/images/transparent.png"),
|
||||
Image.open("Tests/images/7x13.png"),
|
||||
]
|
||||
try:
|
||||
yield ims
|
||||
finally:
|
||||
for im in ims:
|
||||
im.close()
|
||||
|
||||
def setup_module() -> None:
|
||||
ims.append(hopper())
|
||||
ims.append(Image.open("Tests/images/transparent.png"))
|
||||
ims.append(Image.open("Tests/images/7x13.png"))
|
||||
|
||||
|
||||
def teardown_module() -> None:
|
||||
for im in ims:
|
||||
im.close()
|
||||
|
||||
|
||||
def roundtrip(expected: Image.Image) -> None:
|
||||
|
@ -44,26 +42,26 @@ def roundtrip(expected: Image.Image) -> None:
|
|||
assert_image_equal(result, expected.convert("RGB"))
|
||||
|
||||
|
||||
def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None:
|
||||
for im in test_images:
|
||||
def test_sanity_1() -> None:
|
||||
for im in ims:
|
||||
roundtrip(im.convert("1"))
|
||||
|
||||
|
||||
def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None:
|
||||
for im in test_images:
|
||||
def test_sanity_rgb() -> None:
|
||||
for im in ims:
|
||||
roundtrip(im.convert("RGB"))
|
||||
|
||||
|
||||
def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None:
|
||||
for im in test_images:
|
||||
def test_sanity_rgba() -> None:
|
||||
for im in ims:
|
||||
roundtrip(im.convert("RGBA"))
|
||||
|
||||
|
||||
def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None:
|
||||
for im in test_images:
|
||||
def test_sanity_l() -> None:
|
||||
for im in ims:
|
||||
roundtrip(im.convert("L"))
|
||||
|
||||
|
||||
def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None:
|
||||
for im in test_images:
|
||||
def test_sanity_p() -> None:
|
||||
for im in ims:
|
||||
roundtrip(im.convert("P"))
|
||||
|
|
|
@ -14,7 +14,7 @@ def test_sanity() -> None:
|
|||
assert data[0] == (20, 20, 70)
|
||||
|
||||
|
||||
def test_roundtrip() -> None:
|
||||
def test_mode() -> None:
|
||||
def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]:
|
||||
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
|
||||
data = im.getdata()
|
||||
|
|
|
@ -32,7 +32,7 @@ class TestImagingPaste:
|
|||
def assert_9points_paste(
|
||||
self,
|
||||
im: Image.Image,
|
||||
im2: Image.Image,
|
||||
im2: Image.Image | str | tuple[int, ...],
|
||||
mask: Image.Image,
|
||||
expected: list[tuple[int, int, int, int]],
|
||||
) -> None:
|
||||
|
|
|
@ -94,6 +94,19 @@ def test_quantize_dither_diff() -> None:
|
|||
assert dither.tobytes() != nodither.tobytes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
|
||||
)
|
||||
def test_quantize_kmeans(method) -> None:
|
||||
im = hopper()
|
||||
no_kmeans = im.quantize(kmeans=0, method=method)
|
||||
kmeans = im.quantize(kmeans=1, method=method)
|
||||
assert kmeans.tobytes() != no_kmeans.tobytes()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.quantize(kmeans=-1, method=method)
|
||||
|
||||
|
||||
def test_colors() -> None:
|
||||
im = hopper()
|
||||
colors = 2
|
||||
|
|
|
@ -186,7 +186,9 @@ def assert_compare_images(
|
|||
|
||||
bands = ImageMode.getmode(a.mode).bands
|
||||
for band, ach, bch in zip(bands, a.split(), b.split()):
|
||||
ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch)
|
||||
ch_diff = ImageMath.lambda_eval(
|
||||
lambda args: args["convert"](abs(args["a"] - args["b"]), "L"), a=ach, b=bch
|
||||
)
|
||||
ch_hist = ch_diff.histogram()
|
||||
|
||||
average_diff = sum(i * num for i, num in enumerate(ch_hist)) / (
|
||||
|
|
|
@ -237,7 +237,7 @@ class TestCoreResampleConsistency:
|
|||
im = Image.new(mode, (512, 9), fill)
|
||||
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
|
||||
|
||||
def run_case(self, case: tuple[Image.Image, Image.Image]) -> None:
|
||||
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
|
||||
channel, color = case
|
||||
px = channel.load()
|
||||
for x in range(channel.size[0]):
|
||||
|
@ -284,7 +284,7 @@ class TestCoreResampleAlphaCorrect:
|
|||
used_colors = {px[x, y][0] for x in range(i.size[0])}
|
||||
assert 256 == len(used_colors), (
|
||||
"All colors should be present in resized image. "
|
||||
f"Only {len(used_colors)} on {y} line."
|
||||
f"Only {len(used_colors)} on line {y}."
|
||||
)
|
||||
|
||||
@pytest.mark.xfail(reason="Current implementation isn't precise enough")
|
||||
|
|
|
@ -154,7 +154,7 @@ class TestImagingCoreResize:
|
|||
|
||||
def test_unknown_filter(self) -> None:
|
||||
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:
|
||||
# This test is intended for only check for consistent behaviour across
|
||||
|
|
|
@ -4,13 +4,14 @@ import datetime
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageMode, features
|
||||
from PIL import Image, ImageMode, ImageWin, features
|
||||
|
||||
from .helper import (
|
||||
assert_image,
|
||||
|
@ -18,6 +19,7 @@ from .helper import (
|
|||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
is_pypy,
|
||||
)
|
||||
|
||||
try:
|
||||
|
@ -213,6 +215,10 @@ def test_display_profile() -> None:
|
|||
# try fetching the profile for the current display device
|
||||
ImageCms.get_display_profile()
|
||||
|
||||
if sys.platform == "win32":
|
||||
ImageCms.get_display_profile(ImageWin.HDC(0))
|
||||
ImageCms.get_display_profile(ImageWin.HWND(0))
|
||||
|
||||
|
||||
def test_lab_color_profile() -> None:
|
||||
ImageCms.createProfile("LAB", 5000)
|
||||
|
@ -496,16 +502,34 @@ def test_non_ascii_path(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_profile_typesafety() -> None:
|
||||
"""Profile init type safety
|
||||
|
||||
prepatch, these would segfault, postpatch they should emit a typeerror
|
||||
"""
|
||||
|
||||
# does not segfault
|
||||
with pytest.raises(TypeError, match="Invalid type for Profile"):
|
||||
ImageCms.ImageCmsProfile(0).tobytes()
|
||||
with pytest.raises(TypeError, match="Invalid type for Profile"):
|
||||
ImageCms.ImageCmsProfile(1).tobytes()
|
||||
|
||||
# also check core function
|
||||
with pytest.raises(TypeError):
|
||||
ImageCms.core.profile_tobytes(0)
|
||||
with pytest.raises(TypeError):
|
||||
ImageCms.core.profile_tobytes(1)
|
||||
|
||||
if not is_pypy():
|
||||
# core profile should not be directly instantiable
|
||||
with pytest.raises(TypeError):
|
||||
ImageCms.core.CmsProfile()
|
||||
with pytest.raises(TypeError):
|
||||
ImageCms.core.CmsProfile(0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_pypy(), reason="fails on PyPy")
|
||||
def test_transform_typesafety() -> None:
|
||||
# core transform should not be directly instantiable
|
||||
with pytest.raises(TypeError):
|
||||
ImageCms.core.CmsTransform()
|
||||
with pytest.raises(TypeError):
|
||||
ImageCms.core.CmsTransform(0)
|
||||
|
||||
|
||||
def assert_aux_channel_preserved(
|
||||
mode: str, transform_in_place: bool, preserved_channel: str
|
||||
|
@ -637,6 +661,11 @@ def test_auxiliary_channels_isolated() -> None:
|
|||
assert_image_equal(test_image.convert(dst_format[2]), reference_image)
|
||||
|
||||
|
||||
def test_long_modes() -> None:
|
||||
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
|
||||
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
||||
def test_rgb_lab(mode: str) -> None:
|
||||
im = Image.new(mode, (1, 1))
|
||||
|
|
|
@ -753,7 +753,7 @@ def test_rectangle_I16(bbox: Coords) -> None:
|
|||
draw.rectangle(bbox, outline=0xFFFF)
|
||||
|
||||
# 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)
|
||||
|
@ -868,8 +868,10 @@ def test_rounded_rectangle_zero_radius(bbox: Coords) -> None:
|
|||
[
|
||||
((20, 10, 80, 90), "x"),
|
||||
((20, 10, 81, 90), "x_odd"),
|
||||
((20, 10, 81.1, 90), "x_odd"),
|
||||
((10, 20, 90, 80), "y"),
|
||||
((10, 20, 90, 81), "y_odd"),
|
||||
((10, 20, 90, 81.1), "y_odd"),
|
||||
((20, 20, 80, 80), "both"),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -201,12 +202,22 @@ class TestImageFile:
|
|||
|
||||
|
||||
class MockPyDecoder(ImageFile.PyDecoder):
|
||||
def __init__(self, mode: str, *args: Any) -> None:
|
||||
MockPyDecoder.last = self
|
||||
|
||||
super().__init__(mode, *args)
|
||||
|
||||
def decode(self, buffer):
|
||||
# eof
|
||||
return -1, 0
|
||||
|
||||
|
||||
class MockPyEncoder(ImageFile.PyEncoder):
|
||||
def __init__(self, mode: str, *args: Any) -> None:
|
||||
MockPyEncoder.last = self
|
||||
|
||||
super().__init__(mode, *args)
|
||||
|
||||
def encode(self, buffer):
|
||||
return 1, 1, b""
|
||||
|
||||
|
@ -228,19 +239,8 @@ class MockImageFile(ImageFile.ImageFile):
|
|||
class CodecsTest:
|
||||
@classmethod
|
||||
def setup_class(cls) -> None:
|
||||
cls.decoder = MockPyDecoder(None)
|
||||
cls.encoder = MockPyEncoder(None)
|
||||
|
||||
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)
|
||||
Image.register_decoder("MOCK", MockPyDecoder)
|
||||
Image.register_encoder("MOCK", MockPyEncoder)
|
||||
|
||||
|
||||
class TestPyDecoder(CodecsTest):
|
||||
|
@ -251,13 +251,13 @@ class TestPyDecoder(CodecsTest):
|
|||
|
||||
im.load()
|
||||
|
||||
assert self.decoder.state.xoff == xoff
|
||||
assert self.decoder.state.yoff == yoff
|
||||
assert self.decoder.state.xsize == xsize
|
||||
assert self.decoder.state.ysize == ysize
|
||||
assert MockPyDecoder.last.state.xoff == xoff
|
||||
assert MockPyDecoder.last.state.yoff == yoff
|
||||
assert MockPyDecoder.last.state.xsize == xsize
|
||||
assert MockPyDecoder.last.state.ysize == ysize
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
self.decoder.set_as_raw(b"\x00")
|
||||
MockPyDecoder.last.set_as_raw(b"\x00")
|
||||
|
||||
def test_extents_none(self) -> None:
|
||||
buf = BytesIO(b"\x00" * 255)
|
||||
|
@ -267,10 +267,10 @@ class TestPyDecoder(CodecsTest):
|
|||
|
||||
im.load()
|
||||
|
||||
assert self.decoder.state.xoff == 0
|
||||
assert self.decoder.state.yoff == 0
|
||||
assert self.decoder.state.xsize == 200
|
||||
assert self.decoder.state.ysize == 200
|
||||
assert MockPyDecoder.last.state.xoff == 0
|
||||
assert MockPyDecoder.last.state.yoff == 0
|
||||
assert MockPyDecoder.last.state.xsize == 200
|
||||
assert MockPyDecoder.last.state.ysize == 200
|
||||
|
||||
def test_negsize(self) -> None:
|
||||
buf = BytesIO(b"\x00" * 255)
|
||||
|
@ -315,10 +315,10 @@ class TestPyEncoder(CodecsTest):
|
|||
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
|
||||
)
|
||||
|
||||
assert self.encoder.state.xoff == xoff
|
||||
assert self.encoder.state.yoff == yoff
|
||||
assert self.encoder.state.xsize == xsize
|
||||
assert self.encoder.state.ysize == ysize
|
||||
assert MockPyEncoder.last.state.xoff == xoff
|
||||
assert MockPyEncoder.last.state.yoff == yoff
|
||||
assert MockPyEncoder.last.state.xsize == xsize
|
||||
assert MockPyEncoder.last.state.ysize == ysize
|
||||
|
||||
def test_extents_none(self) -> None:
|
||||
buf = BytesIO(b"\x00" * 255)
|
||||
|
@ -329,10 +329,10 @@ class TestPyEncoder(CodecsTest):
|
|||
fp = BytesIO()
|
||||
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
|
||||
|
||||
assert self.encoder.state.xoff == 0
|
||||
assert self.encoder.state.yoff == 0
|
||||
assert self.encoder.state.xsize == 200
|
||||
assert self.encoder.state.ysize == 200
|
||||
assert MockPyEncoder.last.state.xoff == 0
|
||||
assert MockPyEncoder.last.state.yoff == 0
|
||||
assert MockPyEncoder.last.state.xsize == 200
|
||||
assert MockPyEncoder.last.state.ysize == 200
|
||||
|
||||
def test_negsize(self) -> None:
|
||||
buf = BytesIO(b"\x00" * 255)
|
||||
|
@ -340,12 +340,12 @@ class TestPyEncoder(CodecsTest):
|
|||
im = MockImageFile(buf)
|
||||
|
||||
fp = BytesIO()
|
||||
self.encoder.cleanup_called = False
|
||||
MockPyEncoder.last = None
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||
)
|
||||
assert self.encoder.cleanup_called
|
||||
assert MockPyEncoder.last.cleanup_called
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
|
|
|
@ -1,214 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageMath
|
||||
|
||||
|
||||
def pixel(im: Image.Image | int) -> str | int:
|
||||
if isinstance(im, int):
|
||||
return int(im) # hack to deal with booleans
|
||||
|
||||
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
|
||||
|
||||
|
||||
A = Image.new("L", (1, 1), 1)
|
||||
B = Image.new("L", (1, 1), 2)
|
||||
Z = Image.new("L", (1, 1), 0) # Z for zero
|
||||
F = Image.new("F", (1, 1), 3)
|
||||
I = Image.new("I", (1, 1), 4) # noqa: E741
|
||||
|
||||
A2 = A.resize((2, 2))
|
||||
B2 = B.resize((2, 2))
|
||||
|
||||
images = {"A": A, "B": B, "F": F, "I": I}
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
assert ImageMath.eval("1") == 1
|
||||
assert ImageMath.eval("1+A", A=2) == 3
|
||||
assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3"
|
||||
assert pixel(ImageMath.eval("A+B", images)) == "I 3"
|
||||
assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0"
|
||||
assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3"
|
||||
|
||||
|
||||
def test_ops() -> None:
|
||||
assert pixel(ImageMath.eval("-A", images)) == "I -1"
|
||||
assert pixel(ImageMath.eval("+B", images)) == "L 2"
|
||||
|
||||
assert pixel(ImageMath.eval("A+B", images)) == "I 3"
|
||||
assert pixel(ImageMath.eval("A-B", images)) == "I -1"
|
||||
assert pixel(ImageMath.eval("A*B", images)) == "I 2"
|
||||
assert pixel(ImageMath.eval("A/B", images)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B**2", images)) == "I 4"
|
||||
assert pixel(ImageMath.eval("B**33", images)) == "I 2147483647"
|
||||
|
||||
assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0"
|
||||
assert pixel(ImageMath.eval("float(A)-B", images)) == "F -1.0"
|
||||
assert pixel(ImageMath.eval("float(A)*B", images)) == "F 2.0"
|
||||
assert pixel(ImageMath.eval("float(A)/B", images)) == "F 0.5"
|
||||
assert pixel(ImageMath.eval("float(B)**2", images)) == "F 4.0"
|
||||
assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expression",
|
||||
(
|
||||
"exec('pass')",
|
||||
"(lambda: exec('pass'))()",
|
||||
"(lambda: (lambda: exec('pass'))())()",
|
||||
),
|
||||
)
|
||||
def test_prevent_exec(expression: str) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageMath.eval(expression)
|
||||
|
||||
|
||||
def test_prevent_double_underscores() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageMath.eval("1", {"__": None})
|
||||
|
||||
|
||||
def test_prevent_builtins() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None})
|
||||
|
||||
|
||||
def test_logical() -> None:
|
||||
assert pixel(ImageMath.eval("not A", images)) == 0
|
||||
assert pixel(ImageMath.eval("A and B", images)) == "L 2"
|
||||
assert pixel(ImageMath.eval("A or B", images)) == "L 1"
|
||||
|
||||
|
||||
def test_convert() -> None:
|
||||
assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3"
|
||||
assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0"
|
||||
assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)"
|
||||
|
||||
|
||||
def test_compare() -> None:
|
||||
assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1"
|
||||
assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2"
|
||||
assert pixel(ImageMath.eval("A == 1", images)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A == 2", images)) == "I 0"
|
||||
|
||||
|
||||
def test_one_image_larger() -> None:
|
||||
assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3"
|
||||
assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3"
|
||||
|
||||
|
||||
def test_abs() -> None:
|
||||
assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2"
|
||||
|
||||
|
||||
def test_binary_mod() -> None:
|
||||
assert pixel(ImageMath.eval("A%A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B%B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("B%A", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("Z%A", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0"
|
||||
|
||||
|
||||
def test_bitwise_invert() -> None:
|
||||
assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1"
|
||||
assert pixel(ImageMath.eval("~A", A=A)) == "I -2"
|
||||
assert pixel(ImageMath.eval("~B", B=B)) == "I -3"
|
||||
|
||||
|
||||
def test_bitwise_and() -> None:
|
||||
assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1"
|
||||
|
||||
|
||||
def test_bitwise_or() -> None:
|
||||
assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1"
|
||||
|
||||
|
||||
def test_bitwise_xor() -> None:
|
||||
assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0"
|
||||
|
||||
|
||||
def test_bitwise_leftshift() -> None:
|
||||
assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2"
|
||||
|
||||
|
||||
def test_bitwise_rightshift() -> None:
|
||||
assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_eq() -> None:
|
||||
assert pixel(ImageMath.eval("A==A", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.eval("B==B", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_ne() -> None:
|
||||
assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1"
|
||||
|
||||
|
||||
def test_logical_lt() -> None:
|
||||
assert pixel(ImageMath.eval("A<A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B<B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A<B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("B<A", A=A, B=B)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_le() -> None:
|
||||
assert pixel(ImageMath.eval("A<=A", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.eval("B<=B", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A<=B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("B<=A", A=A, B=B)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_gt() -> None:
|
||||
assert pixel(ImageMath.eval("A>A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B>B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1"
|
||||
|
||||
|
||||
def test_logical_ge() -> None:
|
||||
assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1"
|
||||
|
||||
|
||||
def test_logical_equal() -> None:
|
||||
assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.eval("equal(A, B)", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("equal(B, A)", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_not_equal() -> None:
|
||||
assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)) == "I 1"
|
496
Tests/test_imagemath_lambda_eval.py
Normal file
|
@ -0,0 +1,496 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from PIL import Image, ImageMath
|
||||
|
||||
|
||||
def pixel(im: Image.Image | int) -> str | int:
|
||||
if isinstance(im, int):
|
||||
return int(im) # hack to deal with booleans
|
||||
|
||||
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
|
||||
|
||||
|
||||
A = Image.new("L", (1, 1), 1)
|
||||
B = Image.new("L", (1, 1), 2)
|
||||
Z = Image.new("L", (1, 1), 0) # Z for zero
|
||||
F = Image.new("F", (1, 1), 3)
|
||||
I = Image.new("I", (1, 1), 4) # noqa: E741
|
||||
|
||||
A2 = A.resize((2, 2))
|
||||
B2 = B.resize((2, 2))
|
||||
|
||||
images = {"A": A, "B": B, "F": F, "I": I}
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
assert ImageMath.lambda_eval(lambda args: 1) == 1
|
||||
assert ImageMath.lambda_eval(lambda args: 1 + args["A"], A=2) == 3
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B))
|
||||
== "I 3"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images))
|
||||
== "I 3"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["float"](args["A"]) + args["B"], images
|
||||
)
|
||||
)
|
||||
== "F 3.0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["int"](args["float"](args["A"]) + args["B"]), images
|
||||
)
|
||||
)
|
||||
== "I 3"
|
||||
)
|
||||
|
||||
|
||||
def test_ops() -> None:
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1"
|
||||
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images))
|
||||
== "I 3"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images))
|
||||
== "I -1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images))
|
||||
== "I 2"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images))
|
||||
== "I 0"
|
||||
)
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4"
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images))
|
||||
== "I 2147483647"
|
||||
)
|
||||
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["float"](args["A"]) + args["B"], images
|
||||
)
|
||||
)
|
||||
== "F 3.0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["float"](args["A"]) - args["B"], images
|
||||
)
|
||||
)
|
||||
== "F -1.0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["float"](args["A"]) * args["B"], images
|
||||
)
|
||||
)
|
||||
== "F 2.0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["float"](args["A"]) / args["B"], images
|
||||
)
|
||||
)
|
||||
== "F 0.5"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images))
|
||||
== "F 4.0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images)
|
||||
)
|
||||
== "F 8589934592.0"
|
||||
)
|
||||
|
||||
|
||||
def test_logical() -> None:
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images))
|
||||
== "L 2"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images))
|
||||
== "L 1"
|
||||
)
|
||||
|
||||
|
||||
def test_convert() -> None:
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["convert"](args["A"] + args["B"], "L"), images
|
||||
)
|
||||
)
|
||||
== "L 3"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["convert"](args["A"] + args["B"], "1"), images
|
||||
)
|
||||
)
|
||||
== "1 0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["convert"](args["A"] + args["B"], "RGB"), images
|
||||
)
|
||||
)
|
||||
== "RGB (3, 3, 3)"
|
||||
)
|
||||
|
||||
|
||||
def test_compare() -> None:
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["min"](args["A"], args["B"]), images
|
||||
)
|
||||
)
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["max"](args["A"], args["B"]), images
|
||||
)
|
||||
)
|
||||
== "I 2"
|
||||
)
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0"
|
||||
|
||||
|
||||
def test_one_image_larger() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A2, B=B))
|
||||
== "I 3"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B2))
|
||||
== "I 3"
|
||||
)
|
||||
|
||||
|
||||
def test_abs() -> None:
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: abs(args["A"]), A=A)) == "I 1"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: abs(args["B"]), B=B)) == "I 2"
|
||||
|
||||
|
||||
def test_binary_mod() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["A"], A=A)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["B"], B=B)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["B"], A=A, B=B))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["A"], A=A, B=B))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["A"], A=A, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["B"], B=B, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
|
||||
|
||||
def test_bitwise_invert() -> None:
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: ~args["Z"], Z=Z)) == "I -1"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: ~args["A"], A=A)) == "I -2"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: ~args["B"], B=B)) == "I -3"
|
||||
|
||||
|
||||
def test_bitwise_and() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["Z"], A=A, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["A"], A=A, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["Z"], A=A, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["A"], A=A, Z=Z))
|
||||
== "I 1"
|
||||
)
|
||||
|
||||
|
||||
def test_bitwise_or() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["Z"], A=A, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["A"], A=A, Z=Z))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["Z"], A=A, Z=Z))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["A"], A=A, Z=Z))
|
||||
== "I 1"
|
||||
)
|
||||
|
||||
|
||||
def test_bitwise_xor() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["Z"], A=A, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["A"], A=A, Z=Z))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["Z"], A=A, Z=Z))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["A"], A=A, Z=Z))
|
||||
== "I 0"
|
||||
)
|
||||
|
||||
|
||||
def test_bitwise_leftshift() -> None:
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 0, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 1, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 0, A=A)) == "I 1"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 1, A=A)) == "I 2"
|
||||
|
||||
|
||||
def test_bitwise_rightshift() -> None:
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 0, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 1, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 0, A=A)) == "I 1"
|
||||
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 1, A=A)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_eq() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["A"], A=A)) == "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["B"], B=B)) == "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["B"], A=A, B=B))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["A"], A=A, B=B))
|
||||
== "I 0"
|
||||
)
|
||||
|
||||
|
||||
def test_logical_ne() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["A"], A=A)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["B"], B=B)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["B"], A=A, B=B))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["A"], A=A, B=B))
|
||||
== "I 1"
|
||||
)
|
||||
|
||||
|
||||
def test_logical_lt() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["A"], A=A)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["B"], B=B)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["B"], A=A, B=B))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["A"], A=A, B=B))
|
||||
== "I 0"
|
||||
)
|
||||
|
||||
|
||||
def test_logical_le() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["A"], A=A)) == "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["B"], B=B)) == "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["B"], A=A, B=B))
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["A"], A=A, B=B))
|
||||
== "I 0"
|
||||
)
|
||||
|
||||
|
||||
def test_logical_gt() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["A"], A=A)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["B"], B=B)) == "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["B"], A=A, B=B))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["A"], A=A, B=B))
|
||||
== "I 1"
|
||||
)
|
||||
|
||||
|
||||
def test_logical_ge() -> None:
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["A"], A=A)) == "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["B"], B=B)) == "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["B"], A=A, B=B))
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["A"], A=A, B=B))
|
||||
== "I 1"
|
||||
)
|
||||
|
||||
|
||||
def test_logical_equal() -> None:
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(lambda args: args["equal"](args["A"], args["A"]), A=A)
|
||||
)
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(lambda args: args["equal"](args["B"], args["B"]), B=B)
|
||||
)
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(lambda args: args["equal"](args["Z"], args["Z"]), Z=Z)
|
||||
)
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["equal"](args["A"], args["B"]), A=A, B=B
|
||||
)
|
||||
)
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["equal"](args["B"], args["A"]), A=A, B=B
|
||||
)
|
||||
)
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["equal"](args["A"], args["Z"]), A=A, Z=Z
|
||||
)
|
||||
)
|
||||
== "I 0"
|
||||
)
|
||||
|
||||
|
||||
def test_logical_not_equal() -> None:
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["notequal"](args["A"], args["A"]), A=A
|
||||
)
|
||||
)
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["notequal"](args["B"], args["B"]), B=B
|
||||
)
|
||||
)
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["notequal"](args["Z"], args["Z"]), Z=Z
|
||||
)
|
||||
)
|
||||
== "I 0"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["notequal"](args["A"], args["B"]), A=A, B=B
|
||||
)
|
||||
)
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["notequal"](args["B"], args["A"]), A=A, B=B
|
||||
)
|
||||
)
|
||||
== "I 1"
|
||||
)
|
||||
assert (
|
||||
pixel(
|
||||
ImageMath.lambda_eval(
|
||||
lambda args: args["notequal"](args["A"], args["Z"]), A=A, Z=Z
|
||||
)
|
||||
)
|
||||
== "I 1"
|
||||
)
|
221
Tests/test_imagemath_unsafe_eval.py
Normal file
|
@ -0,0 +1,221 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageMath
|
||||
|
||||
|
||||
def pixel(im: Image.Image | int) -> str | int:
|
||||
if isinstance(im, int):
|
||||
return int(im) # hack to deal with booleans
|
||||
|
||||
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
|
||||
|
||||
|
||||
A = Image.new("L", (1, 1), 1)
|
||||
B = Image.new("L", (1, 1), 2)
|
||||
Z = Image.new("L", (1, 1), 0) # Z for zero
|
||||
F = Image.new("F", (1, 1), 3)
|
||||
I = Image.new("I", (1, 1), 4) # noqa: E741
|
||||
|
||||
A2 = A.resize((2, 2))
|
||||
B2 = B.resize((2, 2))
|
||||
|
||||
images = {"A": A, "B": B, "F": F, "I": I}
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
assert ImageMath.unsafe_eval("1") == 1
|
||||
assert ImageMath.unsafe_eval("1+A", A=2) == 3
|
||||
assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3"
|
||||
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3"
|
||||
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0"
|
||||
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3"
|
||||
|
||||
|
||||
def test_eval_deprecated() -> None:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert ImageMath.eval("1") == 1
|
||||
|
||||
|
||||
def test_ops() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1"
|
||||
assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2"
|
||||
|
||||
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3"
|
||||
assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1"
|
||||
assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2"
|
||||
assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4"
|
||||
assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647"
|
||||
|
||||
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0"
|
||||
assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0"
|
||||
assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0"
|
||||
assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5"
|
||||
assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0"
|
||||
assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expression",
|
||||
(
|
||||
"exec('pass')",
|
||||
"(lambda: exec('pass'))()",
|
||||
"(lambda: (lambda: exec('pass'))())()",
|
||||
),
|
||||
)
|
||||
def test_prevent_exec(expression: str) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageMath.unsafe_eval(expression)
|
||||
|
||||
|
||||
def test_prevent_double_underscores() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageMath.unsafe_eval("1", {"__": None})
|
||||
|
||||
|
||||
def test_prevent_builtins() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None})
|
||||
|
||||
|
||||
def test_logical() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("not A", images)) == 0
|
||||
assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2"
|
||||
assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1"
|
||||
|
||||
|
||||
def test_convert() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3"
|
||||
assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0"
|
||||
assert (
|
||||
pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)"
|
||||
)
|
||||
|
||||
|
||||
def test_compare() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2"
|
||||
assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0"
|
||||
|
||||
|
||||
def test_one_image_larger() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A+B", A=A2, B=B)) == "I 3"
|
||||
assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B2)) == "I 3"
|
||||
|
||||
|
||||
def test_abs() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("abs(A)", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("abs(B)", B=B)) == "I 2"
|
||||
|
||||
|
||||
def test_binary_mod() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A%A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B%B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A%B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("B%A", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("Z%A", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("Z%B", B=B, Z=Z)) == "I 0"
|
||||
|
||||
|
||||
def test_bitwise_invert() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("~Z", Z=Z)) == "I -1"
|
||||
assert pixel(ImageMath.unsafe_eval("~A", A=A)) == "I -2"
|
||||
assert pixel(ImageMath.unsafe_eval("~B", B=B)) == "I -3"
|
||||
|
||||
|
||||
def test_bitwise_and() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("Z&Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("Z&A", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A&Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A&A", A=A, Z=Z)) == "I 1"
|
||||
|
||||
|
||||
def test_bitwise_or() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("Z|Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("Z|A", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A|Z", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A|A", A=A, Z=Z)) == "I 1"
|
||||
|
||||
|
||||
def test_bitwise_xor() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("Z^Z", A=A, Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("Z^A", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A^Z", A=A, Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A^A", A=A, Z=Z)) == "I 0"
|
||||
|
||||
|
||||
def test_bitwise_leftshift() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("Z<<0", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("Z<<1", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A<<0", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A<<1", A=A)) == "I 2"
|
||||
|
||||
|
||||
def test_bitwise_rightshift() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("Z>>0", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("Z>>1", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A>>0", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A>>1", A=A)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_eq() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A==A", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("B==B", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A==B", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B==A", A=A, B=B)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_ne() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A!=A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B!=B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A!=B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("B!=A", A=A, B=B)) == "I 1"
|
||||
|
||||
|
||||
def test_logical_lt() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A<A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B<B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A<B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("B<A", A=A, B=B)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_le() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A<=A", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("B<=B", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A<=B", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("B<=A", A=A, B=B)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_gt() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A>A", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B>B", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("A>B", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B>A", A=A, B=B)) == "I 1"
|
||||
|
||||
|
||||
def test_logical_ge() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("A>=A", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("B>=B", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("A>=B", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("B>=A", A=A, B=B)) == "I 1"
|
||||
|
||||
|
||||
def test_logical_equal() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("equal(A, A)", A=A)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("equal(B, B)", B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("equal(Z, Z)", Z=Z)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("equal(A, B)", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("equal(B, A)", A=A, B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("equal(A, Z)", A=A, Z=Z)) == "I 0"
|
||||
|
||||
|
||||
def test_logical_not_equal() -> None:
|
||||
assert pixel(ImageMath.unsafe_eval("notequal(A, A)", A=A)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("notequal(B, B)", B=B)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("notequal(Z, Z)", Z=Z)) == "I 0"
|
||||
assert pixel(ImageMath.unsafe_eval("notequal(A, B)", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("notequal(B, A)", A=A, B=B)) == "I 1"
|
||||
assert pixel(ImageMath.unsafe_eval("notequal(A, Z)", A=A, Z=Z)) == "I 1"
|
|
@ -73,15 +73,16 @@ def test_lut(op: str) -> None:
|
|||
|
||||
|
||||
def test_no_operator_loaded() -> None:
|
||||
im = Image.new("L", (1, 1))
|
||||
mop = ImageMorph.MorphOp()
|
||||
with pytest.raises(Exception) as e:
|
||||
mop.apply(None)
|
||||
mop.apply(im)
|
||||
assert str(e.value) == "No operator loaded"
|
||||
with pytest.raises(Exception) as e:
|
||||
mop.match(None)
|
||||
mop.match(im)
|
||||
assert str(e.value) == "No operator loaded"
|
||||
with pytest.raises(Exception) as e:
|
||||
mop.save_lut(None)
|
||||
mop.save_lut("")
|
||||
assert str(e.value) == "No operator loaded"
|
||||
|
||||
|
||||
|
|
|
@ -13,8 +13,12 @@ from .helper import (
|
|||
)
|
||||
|
||||
|
||||
class Deformer:
|
||||
def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]:
|
||||
class Deformer(ImageOps.SupportsGetMesh):
|
||||
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
|
||||
return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))]
|
||||
|
||||
|
@ -376,6 +380,7 @@ def test_exif_transpose() -> None:
|
|||
else:
|
||||
original_exif = im.info["exif"]
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert_image_similar(base_im, transposed_im, 17)
|
||||
if orientation_im is base_im:
|
||||
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
|
||||
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
||||
assert transposed_im2 is not None
|
||||
assert_image_equal(transposed_im2, transposed_im)
|
||||
|
||||
check(base_im)
|
||||
|
@ -402,6 +408,7 @@ def test_exif_transpose() -> None:
|
|||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
transposed_im._reload_exif()
|
||||
|
@ -414,12 +421,14 @@ def test_exif_transpose() -> None:
|
|||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
# Orientation set directly on Image.Exif
|
||||
im = hopper()
|
||||
im.getexif()[0x0112] = 3
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
|
@ -499,7 +508,7 @@ def test_autocontrast_mask_real_input() -> None:
|
|||
|
||||
|
||||
def test_autocontrast_preserve_tone() -> None:
|
||||
def autocontrast(mode: str, preserve_tone: bool) -> Image.Image:
|
||||
def autocontrast(mode: str, preserve_tone: bool) -> list[int]:
|
||||
im = hopper(mode)
|
||||
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
|
||||
|
||||
|
|
|
@ -28,8 +28,8 @@ def test_filter_api(test_images: dict[str, Image.Image]) -> None:
|
|||
assert i.mode == "RGB"
|
||||
assert i.size == (128, 128)
|
||||
|
||||
test_filter = ImageFilter.UnsharpMask(2.0, 125, 8)
|
||||
i = im.filter(test_filter)
|
||||
test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8)
|
||||
i = im.filter(test_filter2)
|
||||
assert i.mode == "RGB"
|
||||
assert i.size == (128, 128)
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ def test_sanity(tmp_path: Path) -> None:
|
|||
assert index == 1
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
ImageSequence.Iterator(0)
|
||||
ImageSequence.Iterator(0) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_iterator() -> None:
|
||||
|
@ -72,6 +72,7 @@ def test_consecutive() -> None:
|
|||
for frame in ImageSequence.Iterator(im):
|
||||
if first_frame is None:
|
||||
first_frame = frame.copy()
|
||||
assert first_frame is not None
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
assert_image_equal(frame, first_frame)
|
||||
break
|
||||
|
|
|
@ -68,10 +68,11 @@ def test_show_without_viewers() -> None:
|
|||
def test_viewer() -> None:
|
||||
viewer = ImageShow.Viewer()
|
||||
|
||||
assert viewer.get_format(None) is None
|
||||
im = Image.new("L", (1, 1))
|
||||
assert viewer.get_format(im) is None
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
viewer.get_command(None)
|
||||
viewer.get_command("")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("viewer", ImageShow._viewers)
|
||||
|
|
|
@ -15,7 +15,7 @@ class TestLibPack:
|
|||
mode: str,
|
||||
rawmode: str,
|
||||
data: int | bytes,
|
||||
*pixels: int | float | tuple[int, ...],
|
||||
*pixels: float | tuple[int, ...],
|
||||
) -> None:
|
||||
"""
|
||||
data - either raw bytes with data or just number of bytes in rawmode.
|
||||
|
@ -239,7 +239,7 @@ class TestLibUnpack:
|
|||
mode: str,
|
||||
rawmode: str,
|
||||
data: int | bytes,
|
||||
*pixels: int | float | tuple[int, ...],
|
||||
*pixels: float | tuple[int, ...],
|
||||
) -> None:
|
||||
"""
|
||||
data - either raw bytes with data or just number of bytes in rawmode.
|
||||
|
|
|
@ -19,7 +19,7 @@ from PIL import Image
|
|||
# 7
|
||||
# 160
|
||||
|
||||
# one of string.whitespace is not freely convertable into ascii.
|
||||
# one of string.whitespace is not freely convertible into ascii.
|
||||
|
||||
path = "Tests/images/hopper.jpg"
|
||||
|
||||
|
|
|
@ -4,9 +4,16 @@ import os
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
def test_main() -> None:
|
||||
out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args, report",
|
||||
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
|
||||
)
|
||||
def test_main(args, report) -> None:
|
||||
args = [sys.executable, "-m"] + args
|
||||
out = subprocess.check_output(args).decode("utf-8")
|
||||
lines = out.splitlines()
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Pillow ")
|
||||
|
@ -15,9 +22,15 @@ def test_main() -> None:
|
|||
while lines[0].startswith(" "):
|
||||
lines = lines[1:]
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Python modules loaded from ")
|
||||
assert lines[2].startswith("Binary modules loaded from ")
|
||||
assert lines[3] == "-" * 68
|
||||
assert lines[1].startswith("Python executable is")
|
||||
lines = lines[2:]
|
||||
if lines[0].startswith("Environment Python files loaded from"):
|
||||
lines = lines[1:]
|
||||
assert lines[0].startswith("System Python files loaded from")
|
||||
assert lines[1] == "-" * 68
|
||||
assert lines[2].startswith("Python Pillow modules loaded from ")
|
||||
assert lines[3].startswith("Binary Pillow modules loaded from ")
|
||||
assert lines[4] == "-" * 68
|
||||
jpeg = (
|
||||
os.linesep
|
||||
+ "-" * 68
|
||||
|
@ -31,4 +44,4 @@ def test_main() -> None:
|
|||
+ "-" * 68
|
||||
+ os.linesep
|
||||
)
|
||||
assert jpeg in out
|
||||
assert report == (jpeg not in out)
|
||||
|
|
|
@ -78,7 +78,7 @@ def test_basic(tmp_path: Path, mode: str) -> None:
|
|||
|
||||
|
||||
def test_tobytes() -> None:
|
||||
def tobytes(mode: str) -> Image.Image:
|
||||
def tobytes(mode: str) -> bytes:
|
||||
return Image.new(mode, (1, 1), 1).tobytes()
|
||||
|
||||
order = 1 if Image._ENDIAN == "<" else -1
|
||||
|
|
|
@ -47,9 +47,8 @@ def test_tiff_crashes(test_file: str) -> None:
|
|||
with Image.open(test_file) as im:
|
||||
im.load()
|
||||
except FileNotFoundError:
|
||||
if not on_ci():
|
||||
pytest.skip("test image not found")
|
||||
return
|
||||
raise
|
||||
if on_ci():
|
||||
raise
|
||||
pytest.skip("test image not found")
|
||||
except OSError:
|
||||
pass
|
||||
|
|
|
@ -11,41 +11,12 @@ backend_class = build_wheel.__self__.__class__
|
|||
class _CustomBuildMetaBackend(backend_class):
|
||||
def run_setup(self, setup_script="setup.py"):
|
||||
if self.config_settings:
|
||||
for key, values in self.config_settings.items():
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for value in values:
|
||||
sys.argv.append(f"--pillow-configuration={key}={value}")
|
||||
|
||||
def config_has(key, value):
|
||||
settings = self.config_settings.get(key)
|
||||
if settings:
|
||||
if not isinstance(settings, list):
|
||||
settings = [settings]
|
||||
return value in settings
|
||||
|
||||
flags = []
|
||||
for dependency in (
|
||||
"zlib",
|
||||
"jpeg",
|
||||
"tiff",
|
||||
"freetype",
|
||||
"raqm",
|
||||
"lcms",
|
||||
"webp",
|
||||
"webpmux",
|
||||
"jpeg2000",
|
||||
"imagequant",
|
||||
"xcb",
|
||||
):
|
||||
if config_has(dependency, "enable"):
|
||||
flags.append("--enable-" + dependency)
|
||||
elif config_has(dependency, "disable"):
|
||||
flags.append("--disable-" + dependency)
|
||||
for dependency in ("raqm", "fribidi"):
|
||||
if config_has(dependency, "vendor"):
|
||||
flags.append("--vendor-" + dependency)
|
||||
if self.config_settings.get("platform-guessing") == "disable":
|
||||
flags.append("--disable-platform-guessing")
|
||||
if self.config_settings.get("debug") == "true":
|
||||
flags.append("--debug")
|
||||
if flags:
|
||||
sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:]
|
||||
return super().run_setup(setup_script)
|
||||
|
||||
def build_wheel(
|
||||
|
@ -54,5 +25,15 @@ class _CustomBuildMetaBackend(backend_class):
|
|||
self.config_settings = config_settings
|
||||
return super().build_wheel(wheel_directory, config_settings, metadata_directory)
|
||||
|
||||
def build_editable(
|
||||
self, wheel_directory, config_settings=None, metadata_directory=None
|
||||
):
|
||||
self.config_settings = config_settings
|
||||
return super().build_editable(
|
||||
wheel_directory, config_settings, metadata_directory
|
||||
)
|
||||
|
||||
build_wheel = _CustomBuildMetaBackend().build_wheel
|
||||
|
||||
_backend = _CustomBuildMetaBackend()
|
||||
build_wheel = _backend.build_wheel
|
||||
build_editable = _backend.build_editable
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# install libimagequant
|
||||
|
||||
archive_name=libimagequant
|
||||
archive_version=4.2.2
|
||||
archive_version=4.3.0
|
||||
|
||||
archive=$archive_name-$archive_version
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
The Python Imaging Library (PIL) is
|
||||
|
||||
Copyright © 1997-2011 by Secret Labs AB
|
||||
Copyright © 1995-2011 by Fredrik Lundh
|
||||
Copyright © 1995-2011 by Fredrik Lundh and contributors
|
||||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source PIL
|
||||
Software License:
|
||||
|
|
|
@ -46,7 +46,7 @@ clean:
|
|||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
install-sphinx:
|
||||
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph
|
||||
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
|
|
|
@ -23,7 +23,7 @@ Like PIL, Pillow is `licensed under the open source HPND License <https://raw.gi
|
|||
Why a fork?
|
||||
-----------
|
||||
|
||||
PIL is not setuptools compatible. Please see `this Image-SIG post`_ for a more detailed explanation. Also, PIL's current bi-yearly (or greater) release schedule is too infrequent to accommodate the large number and frequency of issues reported.
|
||||
PIL is not setuptools compatible. Please see `this Image-SIG post`_ for a more detailed explanation. Also, PIL's bi-yearly (or greater) release schedule was too infrequent to accommodate the large number and frequency of issues reported.
|
||||
|
||||
.. _this Image-SIG post: https://mail.python.org/pipermail/image-sig/2010-August/006480.html
|
||||
|
||||
|
@ -35,4 +35,4 @@ What about PIL?
|
|||
Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0
|
||||
added Python 3 support and includes many bug fixes from many contributors.
|
||||
|
||||
As more time passes since the last PIL release (1.1.7 in 2009), the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement.
|
||||
The last PIL release was in 2009 (1.1.7) and `no future releases are expected <https://github.com/python-pillow/Pillow/issues/1535>`_. In January 2020, `the PyPI moderators exhausted the PEP 541 process for contacting the PIL project owner <https://github.com/python-pillow/Pillow/issues/1535#issuecomment-570308446>`_ and the `PIL project on PyPI <https://pypi.org/project/PIL>`_ was transferred to the `Pillow team <https://github.com/python-pillow/Pillow/graphs/contributors>`_. The Pillow team has no plans to update the PIL project on PyPI.
|
||||
|
|
12
docs/conf.py
|
@ -22,7 +22,7 @@ import PIL
|
|||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
needs_sphinx = "2.4"
|
||||
needs_sphinx = "7.3"
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
|
@ -34,7 +34,6 @@ extensions = [
|
|||
"sphinx.ext.viewcode",
|
||||
"sphinx_copybutton",
|
||||
"sphinx_inline_tabs",
|
||||
"sphinx_removed_in",
|
||||
"sphinxext.opengraph",
|
||||
]
|
||||
|
||||
|
@ -54,9 +53,10 @@ master_doc = "index"
|
|||
# General information about the project.
|
||||
project = "Pillow (PIL Fork)"
|
||||
copyright = (
|
||||
"1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors"
|
||||
"1995-2011 Fredrik Lundh and contributors, "
|
||||
"2010-2024 Jeffrey A. Clark and contributors."
|
||||
)
|
||||
author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"
|
||||
author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
@ -252,7 +252,7 @@ latex_documents = [
|
|||
master_doc,
|
||||
"PillowPILFork.tex",
|
||||
"Pillow (PIL Fork) Documentation",
|
||||
"Jeffrey A. Clark (Alex)",
|
||||
"Jeffrey A. Clark",
|
||||
"manual",
|
||||
)
|
||||
]
|
||||
|
@ -302,7 +302,7 @@ texinfo_documents = [
|
|||
"Pillow (PIL Fork) Documentation",
|
||||
author,
|
||||
"PillowPILFork",
|
||||
"Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors.",
|
||||
"Pillow is the friendly PIL fork by Jeffrey A. Clark and contributors.",
|
||||
"Miscellaneous",
|
||||
)
|
||||
]
|
||||
|
|