mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-20 21:41:02 +03:00
Merge branch 'main' into jxl-support2
This commit is contained in:
commit
9313587fec
|
@ -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.17.0
|
||||
cibuildwheel==2.18.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
mypy==1.9.0
|
||||
mypy==1.10.0
|
||||
|
|
|
@ -9,6 +9,7 @@ BinPackParameters: false
|
|||
BreakBeforeBraces: Attach
|
||||
ColumnLimit: 88
|
||||
DerivePointerAlignment: false
|
||||
IndentGotoLabels: false
|
||||
IndentWidth: 4
|
||||
Language: Cpp
|
||||
PointerAlignment: Right
|
||||
|
|
15
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
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.
|
||||
|
||||
|
|
4
.github/workflows/test-cygwin.yml
vendored
4
.github/workflows/test-cygwin.yml
vendored
|
@ -55,6 +55,7 @@ jobs:
|
|||
packages: >
|
||||
gcc-g++
|
||||
ghostscript
|
||||
git
|
||||
ImageMagick
|
||||
jpeg
|
||||
libfreetype-devel
|
||||
|
@ -132,11 +133,12 @@ jobs:
|
|||
bash.exe .ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Cygwin
|
||||
name: Cygwin Python 3.${{ matrix.python-minor-version }}
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
20
.github/workflows/test-docker.yml
vendored
20
.github/workflows/test-docker.yml
vendored
|
@ -36,32 +36,31 @@ jobs:
|
|||
docker: [
|
||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||
ubuntu-22.04-jammy-arm64v8,
|
||||
ubuntu-22.04-jammy-ppc64le,
|
||||
ubuntu-22.04-jammy-s390x,
|
||||
ubuntu-24.04-noble-ppc64le,
|
||||
ubuntu-24.04-noble-s390x,
|
||||
# Then run the remainder
|
||||
alpine,
|
||||
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,
|
||||
debian-12-bookworm-amd64,
|
||||
fedora-38-amd64,
|
||||
fedora-39-amd64,
|
||||
fedora-40-amd64,
|
||||
gentoo,
|
||||
ubuntu-20.04-focal-amd64,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
ubuntu-24.04-noble-amd64,
|
||||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-22.04-jammy-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
- docker: "ubuntu-22.04-jammy-ppc64le"
|
||||
- docker: "ubuntu-24.04-noble-ppc64le"
|
||||
qemu-arch: "ppc64le"
|
||||
- docker: "ubuntu-22.04-jammy-s390x"
|
||||
- docker: "ubuntu-24.04-noble-s390x"
|
||||
qemu-arch: "s390x"
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
@ -83,8 +82,8 @@ jobs:
|
|||
|
||||
- name: Docker build
|
||||
run: |
|
||||
# The Pillow user in the docker container is UID 1000
|
||||
sudo chown -R 1000 $GITHUB_WORKSPACE
|
||||
# The Pillow user in the docker container is UID 1001
|
||||
sudo chown -R 1001 $GITHUB_WORKSPACE
|
||||
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
sudo chown -R runner $GITHUB_WORKSPACE
|
||||
|
||||
|
@ -101,11 +100,12 @@ jobs:
|
|||
MATRIX_DOCKER: ${{ matrix.docker }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: GHA_Docker
|
||||
name: ${{ matrix.docker }}
|
||||
gcov: true
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
3
.github/workflows/test-mingw.yml
vendored
3
.github/workflows/test-mingw.yml
vendored
|
@ -85,8 +85,9 @@ jobs:
|
|||
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: "MSYS2 MinGW"
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
|
4
.github/workflows/test-valgrind.yml
vendored
4
.github/workflows/test-valgrind.yml
vendored
|
@ -50,7 +50,7 @@ jobs:
|
|||
|
||||
- name: Build and Run Valgrind
|
||||
run: |
|
||||
# The Pillow user in the docker container is UID 1000
|
||||
sudo chown -R 1000 $GITHUB_WORKSPACE
|
||||
# The Pillow user in the docker container is UID 1001
|
||||
sudo chown -R 1001 $GITHUB_WORKSPACE
|
||||
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
sudo chown -R runner $GITHUB_WORKSPACE
|
||||
|
|
5
.github/workflows/test-windows.yml
vendored
5
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||
|
@ -213,11 +213,12 @@ jobs:
|
|||
shell: pwsh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: ${{ runner.os }} Python ${{ matrix.python-version }}
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
|
@ -57,9 +57,9 @@ jobs:
|
|||
- python-version: "3.10"
|
||||
PYTHONOPTIMIZE: 2
|
||||
# M1 only available for 3.10+
|
||||
- os: "macos-latest"
|
||||
- os: "macos-13"
|
||||
python-version: "3.9"
|
||||
- os: "macos-latest"
|
||||
- os: "macos-13"
|
||||
python-version: "3.8"
|
||||
exclude:
|
||||
- os: "macos-14"
|
||||
|
@ -150,11 +150,12 @@ jobs:
|
|||
.ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
gcov: true
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
4
.github/workflows/wheels-dependencies.sh
vendored
4
.github/workflows/wheels-dependencies.sh
vendored
|
@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=8.3.0
|
||||
HARFBUZZ_VERSION=8.4.0
|
||||
LIBPNG_VERSION=1.6.43
|
||||
JPEGTURBO_VERSION=3.0.2
|
||||
OPENJPEG_VERSION=2.5.2
|
||||
|
@ -72,7 +72,7 @@ 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 [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
|
|
4
.github/workflows/wheels.yml
vendored
4
.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"
|
||||
|
@ -95,7 +97,7 @@ jobs:
|
|||
matrix:
|
||||
include:
|
||||
- name: "macOS x86_64"
|
||||
os: macos-latest
|
||||
os: macos-13
|
||||
cibw_arch: x86_64
|
||||
macosx_deployment_target: "10.10"
|
||||
- name: "macOS arm64"
|
||||
|
|
|
@ -1,35 +1,42 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.0
|
||||
rev: v0.4.3
|
||||
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.4.2
|
||||
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$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v18.1.4
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
exclude: ^src/thirdparty/
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
|
@ -42,13 +49,20 @@ repos:
|
|||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.28.2
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 1.7.0
|
||||
rev: 1.8.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
|
@ -62,5 +76,10 @@ repos:
|
|||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
- id: check-useless-excludes
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
|
|
|
@ -6,6 +6,10 @@ build:
|
|||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3"
|
||||
jobs:
|
||||
post_checkout:
|
||||
- git remote add upstream https://github.com/python-pillow/Pillow.git # For forks
|
||||
- git fetch upstream --tags
|
||||
|
||||
python:
|
||||
install:
|
||||
|
|
81
CHANGES.rst
81
CHANGES.rst
|
@ -2,9 +2,84 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
10.3.0 (unreleased)
|
||||
10.4.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Fix ImagingAccess for I;16N on big-endian #7921
|
||||
[Yay295, radarhere]
|
||||
|
||||
- Support reading P mode TIFF images with padding #7996
|
||||
[radarhere]
|
||||
|
||||
- Deprecate support for libtiff < 4 #7998
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Corrected ImageShow UnixViewer command #7987
|
||||
[radarhere]
|
||||
|
||||
- Use functools.cached_property in ImageStat #7952
|
||||
[nulano, hugovk, radarhere]
|
||||
|
||||
- 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]
|
||||
|
||||
|
@ -4298,7 +4373,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)
|
||||
|
@ -7509,7 +7584,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
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:
|
||||
|
||||
|
|
3
Makefile
3
Makefile
|
@ -2,7 +2,6 @@
|
|||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
python3 setup.py clean
|
||||
rm src/PIL/*.so || true
|
||||
rm -r build || true
|
||||
find . -name __pycache__ | xargs rm -r || true
|
||||
|
@ -78,8 +77,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
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"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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,38 @@ 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()
|
||||
if mode.startswith("BGR;"):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
im = im.convert(mode)
|
||||
else:
|
||||
try:
|
||||
im = im.convert(mode)
|
||||
except ImportError:
|
||||
if mode == "LAB":
|
||||
im = Image.open("Tests/images/hopper.Lab.tif")
|
||||
else:
|
||||
raise
|
||||
return im
|
||||
|
||||
|
||||
def djpeg_available() -> bool:
|
||||
|
|
BIN
Tests/icc/sGrey-v2-nano.icc
Normal file
BIN
Tests/icc/sGrey-v2-nano.icc
Normal file
Binary file not shown.
BIN
Tests/images/9bit.j2k
Normal file
BIN
Tests/images/9bit.j2k
Normal file
Binary file not shown.
BIN
Tests/images/bmp/q/rgb32h52.bmp
Normal file
BIN
Tests/images/bmp/q/rgb32h52.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
Tests/images/bmp/q/rgba32h56.bmp
Normal file
BIN
Tests/images/bmp/q/rgba32h56.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
Tests/images/m13.fits
Normal file
BIN
Tests/images/m13.fits
Normal file
Binary file not shown.
366
Tests/images/m13_gzip.fits
Normal file
366
Tests/images/m13_gzip.fits
Normal file
File diff suppressed because one or more lines are too long
BIN
Tests/images/seek_too_large.tif
Normal file
BIN
Tests/images/seek_too_large.tif
Normal file
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
@ -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:
|
||||
|
|
|
@ -37,6 +37,8 @@ def test_version() -> None:
|
|||
else:
|
||||
assert function(name) == version
|
||||
if name != "PIL":
|
||||
if name == "zlib" and version is not None:
|
||||
version = version.replace(".zlib-ng", "")
|
||||
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
||||
|
||||
for module in features.modules:
|
||||
|
@ -117,9 +119,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 +132,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 +151,4 @@ def test_pilinfo() -> None:
|
|||
+ "-" * 68
|
||||
+ "\n"
|
||||
)
|
||||
assert jpeg in out
|
||||
assert supported_formats == (jpeg in out)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -336,9 +336,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
|
|||
strings = ["something", "else", "baz", "bif"]
|
||||
|
||||
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
|
||||
ending = "Failure with line ending: %s" % (
|
||||
"".join("%s" % ord(s) for s in ending)
|
||||
)
|
||||
ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
|
||||
assert t.readline().strip("\r\n") == "something", ending
|
||||
assert t.readline().strip("\r\n") == "else", ending
|
||||
assert t.readline().strip("\r\n") == "baz", ending
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -825,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:
|
||||
|
|
|
@ -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:
|
||||
|
@ -364,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"
|
||||
|
@ -436,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,
|
||||
|
@ -185,7 +185,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert field in reloaded, f"{field} not in metadata"
|
||||
|
||||
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
|
||||
def test_additional_metadata(self, tmp_path: Path) -> None:
|
||||
def test_additional_metadata(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
# these should not crash. Seriously dummy data, most of it doesn't make
|
||||
# any sense, so we're running up against limits where we're asking
|
||||
# libtiff to do stupid things.
|
||||
|
@ -236,94 +238,109 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
del new_ifd[338]
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
im.save(out, tiffinfo=new_ifd)
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
@pytest.mark.parametrize(
|
||||
"libtiff",
|
||||
(
|
||||
pytest.param(
|
||||
True,
|
||||
marks=pytest.mark.skipif(
|
||||
not getattr(Image.core, "libtiff_support_custom_tags", False),
|
||||
reason="Custom tags not supported by older libtiff",
|
||||
),
|
||||
),
|
||||
False,
|
||||
),
|
||||
)
|
||||
def test_custom_metadata(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||
|
||||
class Tc(NamedTuple):
|
||||
value: Any
|
||||
type: int
|
||||
supported_by_default: bool
|
||||
|
||||
def test_custom_metadata(self, tmp_path: Path) -> None:
|
||||
tc = namedtuple("tc", "value,type,supported_by_default")
|
||||
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),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
libtiffs = [False]
|
||||
if Image.core.libtiff_support_custom_tags:
|
||||
libtiffs.append(True)
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
) -> None:
|
||||
im = hopper()
|
||||
|
||||
for libtiff in libtiffs:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
out = str(tmp_path / "temp.tif")
|
||||
im.save(out, tiffinfo=tiffinfo)
|
||||
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
) -> None:
|
||||
im = hopper()
|
||||
with Image.open(out) as reloaded:
|
||||
for tag, value in tiffinfo.items():
|
||||
reloaded_value = reloaded.tag_v2[tag]
|
||||
if (
|
||||
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
|
||||
and libtiff
|
||||
):
|
||||
# libtiff does not support real RATIONALS
|
||||
assert round(abs(float(reloaded_value) - float(value)), 7) == 0
|
||||
continue
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
im.save(out, tiffinfo=tiffinfo)
|
||||
assert reloaded_value == value
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
for tag, value in tiffinfo.items():
|
||||
reloaded_value = reloaded.tag_v2[tag]
|
||||
if (
|
||||
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
|
||||
and libtiff
|
||||
):
|
||||
# libtiff does not support real RATIONALS
|
||||
assert (
|
||||
round(abs(float(reloaded_value) - float(value)), 7) == 0
|
||||
)
|
||||
continue
|
||||
# Test with types
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
for tag, tagdata in custom.items():
|
||||
ifd[tag] = tagdata.value
|
||||
ifd.tagtype[tag] = tagdata.type
|
||||
check_tags(ifd)
|
||||
|
||||
assert reloaded_value == value
|
||||
# Test without types. This only works for some types, int for example are
|
||||
# always encoded as LONG and not SIGNED_LONG.
|
||||
check_tags(
|
||||
{
|
||||
tag: tagdata.value
|
||||
for tag, tagdata in custom.items()
|
||||
if tagdata.supported_by_default
|
||||
}
|
||||
)
|
||||
|
||||
# Test with types
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
for tag, tagdata in custom.items():
|
||||
ifd[tag] = tagdata.value
|
||||
ifd.tagtype[tag] = tagdata.type
|
||||
check_tags(ifd)
|
||||
|
||||
# Test without types. This only works for some types, int for example are
|
||||
# always encoded as LONG and not SIGNED_LONG.
|
||||
check_tags(
|
||||
{
|
||||
tag: tagdata.value
|
||||
for tag, tagdata in custom.items()
|
||||
if tagdata.supported_by_default
|
||||
}
|
||||
)
|
||||
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")
|
||||
|
@ -333,24 +350,24 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Should not segfault
|
||||
im.save(outfile)
|
||||
|
||||
def test_xmlpacket_tag(self, tmp_path: Path) -> None:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
def test_xmlpacket_tag(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
if 700 in reloaded.tag_v2:
|
||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||
|
||||
def test_int_dpi(self, tmp_path: Path) -> None:
|
||||
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
# issue #1765
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out, dpi=(72, 72))
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.info["dpi"] == (72.0, 72.0)
|
||||
|
||||
|
@ -412,13 +429,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert "temp.tif" == reread.tag_v2[269]
|
||||
assert "temp.tif" == reread.tag[269][0]
|
||||
|
||||
def test_12bit_rawmode(self) -> None:
|
||||
def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Are we generating the same interpretation
|
||||
of the image as Imagemagick is?"""
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/12bit.cropped.tif") as im:
|
||||
im.load()
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False)
|
||||
# to make the target --
|
||||
# convert 12bit.cropped.tif -depth 16 tmp.tif
|
||||
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
|
||||
|
@ -504,12 +521,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, out)
|
||||
|
||||
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||
def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None:
|
||||
def test_palette_save(
|
||||
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out)
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
# colormap/palette tag
|
||||
|
@ -538,9 +556,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with pytest.raises(OSError):
|
||||
os.close(fn)
|
||||
|
||||
def test_multipage(self) -> None:
|
||||
def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# issue #862
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
||||
|
||||
|
@ -559,11 +577,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert im.size == (20, 20)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
|
||||
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_multipage_nframes(self) -> None:
|
||||
def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# issue #862
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
frames = im.n_frames
|
||||
assert frames == 3
|
||||
|
@ -572,10 +588,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Should not raise ValueError: I/O operation on closed file
|
||||
im.load()
|
||||
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_multipage_seek_backwards(self) -> None:
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
im.seek(1)
|
||||
im.load()
|
||||
|
@ -583,24 +597,21 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.seek(0)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
||||
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test__next(self) -> None:
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
assert not im.tag.next
|
||||
im.load()
|
||||
assert not im.tag.next
|
||||
|
||||
def test_4bit(self) -> None:
|
||||
def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
test_file = "Tests/images/hopper_gray_4bpp.tif"
|
||||
original = hopper("L")
|
||||
|
||||
# Act
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open(test_file) as im:
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
# Assert
|
||||
assert im.size == (128, 128)
|
||||
|
@ -640,12 +651,12 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert im2.mode == "L"
|
||||
assert_image_equal(im, im2)
|
||||
|
||||
def test_save_bytesio(self) -> None:
|
||||
def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# PR 1011
|
||||
# Test TIFF saving to io.BytesIO() object.
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
|
||||
# Generate test image
|
||||
pilim = hopper()
|
||||
|
@ -662,9 +673,6 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
save_bytesio("packbits")
|
||||
save_bytesio("tiff_lzw")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_save_ycbcr(self, tmp_path: Path) -> None:
|
||||
im = hopper("YCbCr")
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
@ -684,15 +692,16 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
if Image.core.libtiff_support_custom_tags:
|
||||
assert reloaded.tag_v2[34665] == 125456
|
||||
|
||||
def test_crashing_metadata(self, tmp_path: Path) -> None:
|
||||
def test_crashing_metadata(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
# issue 1597
|
||||
with Image.open("Tests/images/rdf.tif") as im:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
# this shouldn't crash
|
||||
im.save(out, format="TIFF")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
def test_page_number_x_0(self, tmp_path: Path) -> None:
|
||||
# Issue 973
|
||||
|
@ -723,36 +732,41 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Should not raise PermissionError.
|
||||
os.remove(tmpfile)
|
||||
|
||||
def test_read_icc(self) -> None:
|
||||
def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc = img.info.get("icc_profile")
|
||||
assert icc is not None
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc_libtiff = img.info.get("icc_profile")
|
||||
assert icc_libtiff is not None
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
assert icc == icc_libtiff
|
||||
|
||||
def test_write_icc(self, tmp_path: Path) -> None:
|
||||
def check_write(libtiff: bool) -> None:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
@pytest.mark.parametrize(
|
||||
"libtiff",
|
||||
(
|
||||
pytest.param(
|
||||
True,
|
||||
marks=pytest.mark.skipif(
|
||||
not getattr(Image.core, "libtiff_support_custom_tags", False),
|
||||
reason="Custom tags not supported by older libtiff",
|
||||
),
|
||||
),
|
||||
False,
|
||||
),
|
||||
)
|
||||
def test_write_icc(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc_profile = img.info["icc_profile"]
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc_profile = img.info["icc_profile"]
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
img.save(out, icc_profile=icc_profile)
|
||||
with Image.open(out) as reloaded:
|
||||
assert icc_profile == reloaded.info["icc_profile"]
|
||||
|
||||
libtiffs = []
|
||||
if Image.core.libtiff_support_custom_tags:
|
||||
libtiffs.append(True)
|
||||
libtiffs.append(False)
|
||||
|
||||
for libtiff in libtiffs:
|
||||
check_write(libtiff)
|
||||
out = str(tmp_path / "temp.tif")
|
||||
img.save(out, icc_profile=icc_profile)
|
||||
with Image.open(out) as reloaded:
|
||||
assert icc_profile == reloaded.info["icc_profile"]
|
||||
|
||||
def test_multipage_compression(self) -> None:
|
||||
with Image.open("Tests/images/compression.tif") as im:
|
||||
|
@ -830,12 +844,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
|
||||
|
||||
def test_sampleformat_write(self, tmp_path: Path) -> None:
|
||||
def test_sampleformat_write(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
im = Image.new("F", (1, 1))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out)
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.mode == "F"
|
||||
|
@ -1081,15 +1096,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open(out) as im:
|
||||
im.load()
|
||||
|
||||
def test_realloc_overflow(self) -> None:
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
|
||||
with pytest.raises(OSError) as e:
|
||||
im.load()
|
||||
|
||||
# Assert that the error code is IMAGING_CODEC_MEMORY
|
||||
assert str(e.value) == "-9"
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||
|
|
39
Tests/test_file_mpeg.py
Normal file
39
Tests/test_file_mpeg.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, MpegImagePlugin
|
||||
|
||||
|
||||
def test_identify() -> None:
|
||||
# Arrange
|
||||
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
|
||||
|
||||
# Act
|
||||
with Image.open(b) as im:
|
||||
# Assert
|
||||
assert im.format == "MPEG"
|
||||
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (16, 1)
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(SyntaxError):
|
||||
MpegImagePlugin.MpegImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_load() -> None:
|
||||
# Arrange
|
||||
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
|
||||
|
||||
with Image.open(b) as im:
|
||||
# Act / Assert: cannot load
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
|
@ -93,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)
|
||||
|
||||
|
|
|
@ -85,7 +85,9 @@ class TestFilePng:
|
|||
|
||||
def test_sanity(self, tmp_path: Path) -> None:
|
||||
# internal version number
|
||||
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
|
||||
assert re.search(
|
||||
r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib")
|
||||
)
|
||||
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -28,43 +28,26 @@ from .helper import (
|
|||
assert_image_similar_tofile,
|
||||
assert_not_all_same,
|
||||
hopper,
|
||||
is_big_endian,
|
||||
is_win32,
|
||||
mark_if_feature_version,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
|
||||
# Deprecation helper
|
||||
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
|
||||
if mode.startswith("BGR;"):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
return Image.new(mode, size)
|
||||
else:
|
||||
return Image.new(mode, size)
|
||||
|
||||
|
||||
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.MODES + ["BGR;15", "BGR;16", "BGR;24"])
|
||||
def test_image_modes_success(self, mode: str) -> None:
|
||||
Image.new(mode, (1, 1))
|
||||
helper_image_new(mode, (1, 1))
|
||||
|
||||
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
||||
def test_image_modes_fail(self, mode: str) -> None:
|
||||
|
@ -1042,6 +1025,38 @@ class TestImage:
|
|||
assert im.fp is None
|
||||
|
||||
|
||||
class TestImageBytes:
|
||||
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
|
||||
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
source_bytes = im.tobytes()
|
||||
|
||||
if mode.startswith("BGR;"):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
reloaded = Image.frombytes(mode, im.size, source_bytes)
|
||||
else:
|
||||
reloaded = Image.frombytes(mode, im.size, source_bytes)
|
||||
assert reloaded.tobytes() == source_bytes
|
||||
|
||||
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
|
||||
def test_roundtrip_bytes_method(self, mode: str) -> None:
|
||||
im = hopper(mode)
|
||||
source_bytes = im.tobytes()
|
||||
|
||||
reloaded = helper_image_new(mode, im.size)
|
||||
reloaded.frombytes(source_bytes)
|
||||
assert reloaded.tobytes() == source_bytes
|
||||
|
||||
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
|
||||
def test_getdata_putdata(self, mode: str) -> None:
|
||||
if is_big_endian() and mode == "BGR;15":
|
||||
pytest.xfail("Known failure of BGR;15 on big-endian")
|
||||
im = hopper(mode)
|
||||
reloaded = helper_image_new(mode, im.size)
|
||||
reloaded.putdata(im.getdata())
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
|
||||
class MockEncoder(ImageFile.PyEncoder):
|
||||
pass
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ except ImportError:
|
|||
|
||||
|
||||
class AccessTest:
|
||||
# initial value
|
||||
# Initial value
|
||||
_init_cffi_access = Image.USE_CFFI_ACCESS
|
||||
_need_cffi_access = False
|
||||
|
||||
|
@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest):
|
|||
if bands == 1:
|
||||
return 1
|
||||
if mode in ("BGR;15", "BGR;16"):
|
||||
# These modes have less than 8 bits per band
|
||||
# So (1, 2, 3) cannot be roundtripped
|
||||
# These modes have less than 8 bits per band,
|
||||
# so (1, 2, 3) cannot be roundtripped.
|
||||
return (16, 32, 49)
|
||||
return tuple(range(1, bands + 1))
|
||||
|
||||
|
@ -151,7 +151,7 @@ class TestImageGetPixel(AccessTest):
|
|||
self.color(mode) if expected_color_int is None else expected_color_int
|
||||
)
|
||||
|
||||
# check putpixel
|
||||
# Check putpixel
|
||||
im = Image.new(mode, (1, 1), None)
|
||||
im.putpixel((0, 0), expected_color)
|
||||
actual_color = im.getpixel((0, 0))
|
||||
|
@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest):
|
|||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# check putpixel negative index
|
||||
# Check putpixel negative index
|
||||
im.putpixel((-1, -1), expected_color)
|
||||
actual_color = im.getpixel((-1, -1))
|
||||
assert actual_color == expected_color, (
|
||||
|
@ -168,22 +168,21 @@ class TestImageGetPixel(AccessTest):
|
|||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# Check 0
|
||||
# Check 0x0 image with None initial color
|
||||
im = Image.new(mode, (0, 0), None)
|
||||
assert im.load() is not None
|
||||
|
||||
error = ValueError if self._need_cffi_access else IndexError
|
||||
with pytest.raises(error):
|
||||
im.putpixel((0, 0), expected_color)
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
# Check negative index
|
||||
with pytest.raises(error):
|
||||
im.putpixel((-1, -1), expected_color)
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
# check initial color
|
||||
# Check initial color
|
||||
im = Image.new(mode, (1, 1), expected_color)
|
||||
actual_color = im.getpixel((0, 0))
|
||||
assert actual_color == expected_color, (
|
||||
|
@ -191,46 +190,30 @@ class TestImageGetPixel(AccessTest):
|
|||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# check initial color negative index
|
||||
# Check initial color negative index
|
||||
actual_color = im.getpixel((-1, -1))
|
||||
assert actual_color == expected_color, (
|
||||
f"initial color failed with negative index for mode {mode}, "
|
||||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# Check 0
|
||||
# Check 0x0 image with initial color
|
||||
im = Image.new(mode, (0, 0), expected_color)
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
# Check negative index
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
(
|
||||
"1",
|
||||
"L",
|
||||
"LA",
|
||||
"I",
|
||||
"I;16",
|
||||
"I;16B",
|
||||
"F",
|
||||
"P",
|
||||
"PA",
|
||||
"BGR;15",
|
||||
"BGR;16",
|
||||
"BGR;24",
|
||||
"RGB",
|
||||
"RGBA",
|
||||
"RGBX",
|
||||
"CMYK",
|
||||
"YCbCr",
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("mode", Image.MODES)
|
||||
def test_basic(self, mode: str) -> None:
|
||||
self.check(mode)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
|
||||
def test_deprecated(self, mode: str) -> None:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
self.check(mode)
|
||||
|
||||
def test_list(self) -> None:
|
||||
im = hopper()
|
||||
assert im.getpixel([0, 0]) == (20, 20, 70)
|
||||
|
@ -238,7 +221,7 @@ class TestImageGetPixel(AccessTest):
|
|||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
|
||||
def test_signedness(self, mode: str, expected_color: int) -> None:
|
||||
# see https://github.com/python-pillow/Pillow/issues/452
|
||||
# See https://github.com/python-pillow/Pillow/issues/452
|
||||
# pixelaccess is using signed int* instead of uint*
|
||||
self.check(mode, expected_color)
|
||||
|
||||
|
@ -298,13 +281,6 @@ class TestCffi(AccessTest):
|
|||
im = Image.new(mode, (10, 10), 40000)
|
||||
self._test_get_access(im)
|
||||
|
||||
# These don't actually appear to be modes that I can actually make,
|
||||
# as unpack sets them directly into the I mode.
|
||||
# im = Image.new('I;32L', (10, 10), -2**10)
|
||||
# self._test_get_access(im)
|
||||
# im = Image.new('I;32B', (10, 10), 2**10)
|
||||
# self._test_get_access(im)
|
||||
|
||||
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
|
||||
"""Are we writing the correct bits into the image?
|
||||
|
||||
|
@ -336,23 +312,18 @@ class TestCffi(AccessTest):
|
|||
self._test_set_access(hopper("LA"), (128, 128))
|
||||
self._test_set_access(hopper("1"), 255)
|
||||
self._test_set_access(hopper("P"), 128)
|
||||
# self._test_set_access(i, (128, 128)) #PA -- undone how to make
|
||||
self._test_set_access(hopper("PA"), (128, 128))
|
||||
self._test_set_access(hopper("F"), 1024.0)
|
||||
|
||||
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
|
||||
im = Image.new(mode, (10, 10), 40000)
|
||||
self._test_set_access(im, 45000)
|
||||
|
||||
# im = Image.new('I;32L', (10, 10), -(2**10))
|
||||
# self._test_set_access(im, -(2**13)+1)
|
||||
# im = Image.new('I;32B', (10, 10), 2**10)
|
||||
# self._test_set_access(im, 2**13-1)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||
def test_not_implemented(self) -> None:
|
||||
assert PyAccess.new(hopper("BGR;15")) is None
|
||||
|
||||
# ref https://github.com/python-pillow/Pillow/pull/2009
|
||||
# Ref https://github.com/python-pillow/Pillow/pull/2009
|
||||
def test_reference_counting(self) -> None:
|
||||
size = 10
|
||||
|
||||
|
@ -361,7 +332,7 @@ class TestCffi(AccessTest):
|
|||
with pytest.warns(DeprecationWarning):
|
||||
px = Image.new("L", (size, 1), 0).load()
|
||||
for i in range(size):
|
||||
# pixels can contain garbage if image is released
|
||||
# Pixels can contain garbage if image is released
|
||||
assert px[i, 0] == 0
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
|
@ -439,13 +410,14 @@ class TestEmbeddable:
|
|||
from setuptools.command import build_ext
|
||||
|
||||
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
||||
home = sys.prefix.replace("\\", "\\\\")
|
||||
fh.write(
|
||||
"""
|
||||
f"""
|
||||
#include "Python.h"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
char *home = "%s";
|
||||
{{
|
||||
char *home = "{home}";
|
||||
wchar_t *whome = Py_DecodeLocale(home, NULL);
|
||||
Py_SetPythonHome(whome);
|
||||
|
||||
|
@ -460,9 +432,8 @@ int main(int argc, char* argv[])
|
|||
PyMem_RawFree(whome);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}}
|
||||
"""
|
||||
% sys.prefix.replace("\\", "\\\\")
|
||||
)
|
||||
|
||||
compiler = getattr(build_ext, "new_compiler")()
|
||||
|
@ -478,7 +449,7 @@ int main(int argc, char* argv[])
|
|||
env = os.environ.copy()
|
||||
env["PATH"] = sys.prefix + ";" + env["PATH"]
|
||||
|
||||
# do not display the Windows Error Reporting dialog
|
||||
# Do not display the Windows Error Reporting dialog
|
||||
getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
|
||||
|
||||
process = subprocess.Popen(["embed_pil.exe"], env=env)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -16,11 +16,13 @@ pytestmark = pytest.mark.skipif(
|
|||
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
||||
)
|
||||
|
||||
ims = [
|
||||
hopper(),
|
||||
Image.open("Tests/images/transparent.png"),
|
||||
Image.open("Tests/images/7x13.png"),
|
||||
]
|
||||
ims: list[Image.Image] = []
|
||||
|
||||
|
||||
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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -81,7 +81,8 @@ def test_mode_F() -> None:
|
|||
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
|
||||
def test_mode_BGR(mode: str) -> None:
|
||||
data = [(16, 32, 49), (32, 32, 98)]
|
||||
im = Image.new(mode, (1, 2))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
im = Image.new(mode, (1, 2))
|
||||
im.putdata(data)
|
||||
|
||||
assert list(im.getdata()) == data
|
||||
|
|
|
@ -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)) / (
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
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
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"
|
|
@ -47,7 +47,11 @@ def test_iterator_min_frame() -> None:
|
|||
assert i[index] == next(i)
|
||||
|
||||
|
||||
def _test_multipage_tiff() -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
|
||||
)
|
||||
def test_multipage_tiff(monkeypatch: pytest.MonkeyPatch, libtiff: bool) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", libtiff)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
for index, frame in enumerate(ImageSequence.Iterator(im)):
|
||||
frame.load()
|
||||
|
@ -55,17 +59,6 @@ def _test_multipage_tiff() -> None:
|
|||
frame.convert("RGB")
|
||||
|
||||
|
||||
def test_tiff() -> None:
|
||||
_test_multipage_tiff()
|
||||
|
||||
|
||||
@skip_unless_feature("libtiff")
|
||||
def test_libtiff() -> None:
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
_test_multipage_tiff()
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
|
||||
def test_consecutive() -> None:
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
first_frame = None
|
||||
|
|
|
@ -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.
|
||||
|
@ -216,7 +216,10 @@ class TestLibPack:
|
|||
)
|
||||
|
||||
def test_I16(self) -> None:
|
||||
self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605)
|
||||
if sys.byteorder == "little":
|
||||
self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605)
|
||||
else:
|
||||
self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506)
|
||||
|
||||
def test_F_float(self) -> None:
|
||||
self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34)
|
||||
|
@ -239,7 +242,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.
|
||||
|
@ -359,11 +362,14 @@ class TestLibUnpack:
|
|||
)
|
||||
|
||||
def test_BGR(self) -> None:
|
||||
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
|
||||
self.assert_unpack(
|
||||
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
|
||||
)
|
||||
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
self.assert_unpack(
|
||||
"BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)
|
||||
)
|
||||
self.assert_unpack(
|
||||
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
|
||||
)
|
||||
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
|
||||
|
||||
def test_RGBA(self) -> None:
|
||||
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,10 +3,12 @@ from __future__ import annotations
|
|||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, TiffImagePlugin, features
|
||||
import pytest
|
||||
|
||||
from PIL import Image, TiffImagePlugin
|
||||
from PIL.TiffImagePlugin import IFDRational
|
||||
|
||||
from .helper import hopper
|
||||
from .helper import hopper, skip_unless_feature
|
||||
|
||||
|
||||
def _test_equal(num, denom, target) -> None:
|
||||
|
@ -52,18 +54,18 @@ def test_nonetype() -> None:
|
|||
assert xres and yres
|
||||
|
||||
|
||||
def test_ifd_rational_save(tmp_path: Path) -> None:
|
||||
methods = [True]
|
||||
if features.check("libtiff"):
|
||||
methods.append(False)
|
||||
@pytest.mark.parametrize(
|
||||
"libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
|
||||
)
|
||||
def test_ifd_rational_save(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||
) -> None:
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
res = IFDRational(301, 1)
|
||||
|
||||
for libtiff in methods:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||
im.save(out, dpi=(res, res), compression="raw")
|
||||
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
res = IFDRational(301, 1)
|
||||
im.save(out, dpi=(res, res), compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
|
||||
with Image.open(out) as reloaded:
|
||||
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
13
docs/conf.py
13
docs/conf.py
|
@ -22,19 +22,19 @@ 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
|
||||
# ones.
|
||||
extensions = [
|
||||
"dater",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx_copybutton",
|
||||
"sphinx_inline_tabs",
|
||||
"sphinx_removed_in",
|
||||
"sphinxext.opengraph",
|
||||
]
|
||||
|
||||
|
@ -54,9 +54,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 +253,7 @@ latex_documents = [
|
|||
master_doc,
|
||||
"PillowPILFork.tex",
|
||||
"Pillow (PIL Fork) Documentation",
|
||||
"Jeffrey A. Clark (Alex)",
|
||||
"Jeffrey A. Clark",
|
||||
"manual",
|
||||
)
|
||||
]
|
||||
|
@ -302,7 +303,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",
|
||||
)
|
||||
]
|
||||
|
|
48
docs/dater.py
Normal file
48
docs/dater.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
"""
|
||||
Sphinx extension to add timestamps to release notes based on Git versions.
|
||||
|
||||
Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sphinx.application import Sphinx
|
||||
|
||||
DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+")
|
||||
VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n")
|
||||
|
||||
|
||||
def get_date_for(git_version: str) -> str | None:
|
||||
cmd = ["git", "log", "-1", "--format=%ai", git_version]
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8"
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
return out.split()[0]
|
||||
|
||||
|
||||
def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
|
||||
if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])):
|
||||
old_title = m.group(1)
|
||||
|
||||
if tag_date := get_date_for(old_title):
|
||||
new_title = f"{old_title} ({tag_date})"
|
||||
else:
|
||||
new_title = f"{old_title} (unreleased)"
|
||||
|
||||
new_underline = "-" * len(new_title)
|
||||
|
||||
result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1)
|
||||
source[0] = result
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> dict[str, bool]:
|
||||
app.connect("source-read", add_date)
|
||||
return {"parallel_read_safe": True}
|
|
@ -92,6 +92,29 @@ Deprecated Use instead
|
|||
:py:data:`sys.version_info`, and ``PIL.__version__``
|
||||
============================================ ====================================================
|
||||
|
||||
ImageMath eval()
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 10.3.0
|
||||
|
||||
``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or
|
||||
:py:meth:`~PIL.ImageMath.unsafe_eval` instead.
|
||||
|
||||
BGR;15, BGR 16 and BGR;24
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 10.4.0
|
||||
|
||||
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
|
||||
|
||||
Support for LibTIFF earlier than 4
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 10.4.0
|
||||
|
||||
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||
Upgrade to a newer version of LibTIFF instead.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -59,9 +59,6 @@ Pillow also provides limited support for a few additional modes, including:
|
|||
* ``I;16L`` (16-bit little endian unsigned integer pixels)
|
||||
* ``I;16B`` (16-bit big endian unsigned integer pixels)
|
||||
* ``I;16N`` (16-bit native endian unsigned integer pixels)
|
||||
* ``BGR;15`` (15-bit reversed true colour)
|
||||
* ``BGR;16`` (16-bit reversed true colour)
|
||||
* ``BGR;24`` (24-bit reversed true colour)
|
||||
|
||||
Premultiplied alpha is where the values for each other channel have been
|
||||
multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)``
|
||||
|
|
|
@ -1234,11 +1234,15 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
If present and true, instructs the WebP writer to use lossless compression.
|
||||
|
||||
**quality**
|
||||
Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest
|
||||
Integer, 0-100, defaults to 80. For lossy, 0 gives the smallest
|
||||
size and 100 the largest. For lossless, this parameter is the amount
|
||||
of effort put into the compression: 0 is the fastest, but gives larger
|
||||
files compared to the slowest, but best, 100.
|
||||
|
||||
**alpha_quality**
|
||||
Integer, 0-100, defaults to 100. For lossy compression only. 0 gives the
|
||||
smallest size and 100 is lossless.
|
||||
|
||||
**method**
|
||||
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4.
|
||||
|
||||
|
@ -1335,7 +1339,8 @@ FITS
|
|||
|
||||
.. versionadded:: 9.1.0
|
||||
|
||||
Pillow identifies and reads FITS files, commonly used for astronomy.
|
||||
Pillow identifies and reads FITS files, commonly used for astronomy. Uncompressed and
|
||||
GZIP_1 compressed images can be read.
|
||||
|
||||
FLI, FLC
|
||||
^^^^^^^^
|
||||
|
@ -1351,9 +1356,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
|
|||
FPX
|
||||
^^^
|
||||
|
||||
Pillow reads Kodak FlashPix files. In the current version, only the highest
|
||||
resolution image is read from the file, and the viewing transform is not taken
|
||||
into account.
|
||||
Pillow reads Kodak FlashPix files. Only the highest resolution image is read from the
|
||||
file, and the viewing transform is not taken into account.
|
||||
|
||||
To enable FPX support, you must install :pypi:`olefile`.
|
||||
|
||||
|
@ -1484,7 +1488,9 @@ QOI
|
|||
|
||||
.. versionadded:: 9.5.0
|
||||
|
||||
Pillow identifies and reads images in Quite OK Image format.
|
||||
Pillow reads images in Quite OK Image format using a Python decoder. If you wish to
|
||||
write code specifically for this format, :pypi:`qoi` is an alternative library that
|
||||
uses C to decode the image and interfaces with NumPy.
|
||||
|
||||
SUN
|
||||
^^^
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Pillow
|
||||
======
|
||||
|
||||
Pillow is the friendly PIL fork by `Jeffrey A. Clark (Alex) and contributors <https://github.com/python-pillow/Pillow/graphs/contributors>`_. PIL is the Python Imaging Library by Fredrik Lundh and contributors.
|
||||
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.
|
||||
|
||||
Pillow for enterprise is available via the Tidelift Subscription. `Learn more <https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=docs&utm_campaign=enterprise>`_.
|
||||
|
||||
|
|
|
@ -266,9 +266,10 @@ After navigating to the Pillow directory, run::
|
|||
Build Options
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
|
||||
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
|
||||
sets the number of CPUs to use, or can disable parallel building by
|
||||
* Config setting: ``-C parallel=n``. Can also be given
|
||||
with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
|
||||
multiprocessing to build the extension. Setting ``-C parallel=n``
|
||||
sets the number of CPUs to use to ``n``, or can disable parallel building by
|
||||
using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
|
||||
available, as many as are present.
|
||||
|
||||
|
@ -293,14 +294,13 @@ Build Options
|
|||
used to compile the standard Pillow wheels. Compiling libraqm requires
|
||||
a C99-compliant compiler.
|
||||
|
||||
* Build flag: ``-C platform-guessing=disable``. Skips all of the
|
||||
* Config setting: ``-C platform-guessing=disable``. Skips all of the
|
||||
platform dependent guessing of include and library directories for
|
||||
automated build systems that configure the proper paths in the
|
||||
environment variables (e.g. Buildroot).
|
||||
|
||||
* Build flag: ``-C debug=true``. Adds a debugging flag to the include and
|
||||
library search process to dump all paths searched for and found to
|
||||
stdout.
|
||||
* Config setting: ``-C debug=true``. Adds a debugging flag to the include and
|
||||
library search process to dump all paths searched for and found to stdout.
|
||||
|
||||
|
||||
Sample usage::
|
||||
|
|
|
@ -25,23 +25,19 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Arch | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS 7 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS Stream 8 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS Stream 9 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 11 Bullseye | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 38 | 3.11 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 39 | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 40 | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Gentoo | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| macOS 12 Monterey | 3.8, 3.9 | x86-64 |
|
||||
| macOS 13 Ventura | 3.8, 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
|
||||
| | PyPy3 | |
|
||||
|
@ -51,7 +47,9 @@ These platforms are built and tested for every change.
|
|||
| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
|
||||
| | 3.12, 3.13, PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.10 | arm64v8, ppc64le, |
|
||||
| | 3.10 | arm64v8 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
|
||||
| | | s390x |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2016 | 3.8 | x86-64 |
|
||||
|
@ -81,7 +79,7 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+============================+==================+==============+
|
||||
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm |
|
||||
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
|
|
|
@ -78,6 +78,8 @@ Constructing images
|
|||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autofunction:: new
|
||||
.. autoclass:: SupportsArrayInterface
|
||||
:show-inheritance:
|
||||
.. autofunction:: fromarray
|
||||
.. autofunction:: frombytes
|
||||
.. autofunction:: frombuffer
|
||||
|
|
|
@ -73,7 +73,7 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
:canonical: PIL._imagingcms.CmsProfile
|
||||
|
||||
.. py:attribute:: creation_date
|
||||
:type: Optional[datetime.datetime]
|
||||
:type: datetime.datetime | None
|
||||
|
||||
Date and time this profile was first created (see 7.2.1 of ICC.1:2010).
|
||||
|
||||
|
@ -156,58 +156,58 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
not been calculated (see 7.2.18 of ICC.1:2010).
|
||||
|
||||
.. py:attribute:: copyright
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
The text copyright information for the profile (see 9.2.21 of ICC.1:2010).
|
||||
|
||||
.. py:attribute:: manufacturer
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
The (English) display string for the device manufacturer (see
|
||||
9.2.22 of ICC.1:2010).
|
||||
|
||||
.. py:attribute:: model
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
The (English) display string for the device model of the device
|
||||
for which this profile is created (see 9.2.23 of ICC.1:2010).
|
||||
|
||||
.. py:attribute:: profile_description
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
The (English) display string for the profile description (see
|
||||
9.2.41 of ICC.1:2010).
|
||||
|
||||
.. py:attribute:: target
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
The name of the registered characterization data set, or the
|
||||
measurement data for a characterization target (see 9.2.14 of
|
||||
ICC.1:2010).
|
||||
|
||||
.. py:attribute:: red_colorant
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010).
|
||||
|
||||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: green_colorant
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010).
|
||||
|
||||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: blue_colorant
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010).
|
||||
|
||||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: luminance
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The absolute luminance of emissive devices in candelas per square
|
||||
metre as described by the Y channel (see 9.2.32 of ICC.1:2010).
|
||||
|
@ -215,7 +215,7 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: chromaticity
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The data of the phosphor/colorant chromaticity set used (red,
|
||||
green and blue channels, see 9.2.16 of ICC.1:2010).
|
||||
|
@ -223,7 +223,7 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
The value is in the format ``((x, y, Y), (x, y, Y), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: chromatic_adaption
|
||||
:type: tuple[tuple[float]]
|
||||
:type: tuple[tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]], tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]]] | None
|
||||
|
||||
The chromatic adaption matrix converts a color measured using the
|
||||
actual illumination conditions and relative to the actual adopted
|
||||
|
@ -249,34 +249,34 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
9.2.19 of ICC.1:2010).
|
||||
|
||||
.. py:attribute:: colorimetric_intent
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
4-character string (padded with whitespace) identifying the image
|
||||
state of PCS colorimetry produced using the colorimetric intent
|
||||
transforms (see 9.2.20 of ICC.1:2010 for details).
|
||||
|
||||
.. py:attribute:: perceptual_rendering_intent_gamut
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
4-character string (padded with whitespace) identifying the (one)
|
||||
standard reference medium gamut (see 9.2.37 of ICC.1:2010 for
|
||||
details).
|
||||
|
||||
.. py:attribute:: saturation_rendering_intent_gamut
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
4-character string (padded with whitespace) identifying the (one)
|
||||
standard reference medium gamut (see 9.2.37 of ICC.1:2010 for
|
||||
details).
|
||||
|
||||
.. py:attribute:: technology
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
4-character string (padded with whitespace) identifying the device
|
||||
technology (see 9.2.47 of ICC.1:2010 for details).
|
||||
|
||||
.. py:attribute:: media_black_point
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
This tag specifies the media black point and is used for
|
||||
generating absolute colorimetry.
|
||||
|
@ -287,19 +287,19 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: media_white_point_temperature
|
||||
:type: Optional[float]
|
||||
:type: float | None
|
||||
|
||||
Calculates the white point temperature (see the LCMS documentation
|
||||
for more information).
|
||||
|
||||
.. py:attribute:: viewing_condition
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
The (English) display string for the viewing conditions (see
|
||||
9.2.48 of ICC.1:2010).
|
||||
|
||||
.. py:attribute:: screening_description
|
||||
:type: Optional[str]
|
||||
:type: str | None
|
||||
|
||||
The (English) display string for the screening conditions.
|
||||
|
||||
|
@ -307,21 +307,21 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
version 4.
|
||||
|
||||
.. py:attribute:: red_primary
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The XYZ-transformed of the RGB primary color red (1, 0, 0).
|
||||
|
||||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: green_primary
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The XYZ-transformed of the RGB primary color green (0, 1, 0).
|
||||
|
||||
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
|
||||
|
||||
.. py:attribute:: blue_primary
|
||||
:type: Optional[tuple[tuple[float]]]
|
||||
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
|
||||
|
||||
The XYZ-transformed of the RGB primary color blue (0, 0, 1).
|
||||
|
||||
|
@ -334,7 +334,7 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
documentation on LCMS).
|
||||
|
||||
.. py:attribute:: clut
|
||||
:type: dict[tuple[bool]]
|
||||
:type: dict[int, tuple[bool, bool, bool]] | None
|
||||
|
||||
Returns a dictionary of all supported intents and directions for
|
||||
the CLUT model.
|
||||
|
@ -353,7 +353,7 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
that intent is supported for that direction.
|
||||
|
||||
.. py:attribute:: intent_supported
|
||||
:type: dict[tuple[bool]]
|
||||
:type: dict[int, tuple[bool, bool, bool]] | None
|
||||
|
||||
Returns a dictionary of all supported intents and directions.
|
||||
|
||||
|
@ -372,7 +372,7 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
|
||||
There is one function defined on the class:
|
||||
|
||||
.. py:method:: is_intent_supported(intent, direction)
|
||||
.. py:method:: is_intent_supported(intent: int, direction: int, /)
|
||||
|
||||
Returns if the intent is supported for the given direction.
|
||||
|
||||
|
|
|
@ -23,8 +23,7 @@ Example: Filter an image
|
|||
Filters
|
||||
-------
|
||||
|
||||
The current version of the library provides the following set of predefined
|
||||
image enhancement filters:
|
||||
Pillow provides the following set of predefined image enhancement filters:
|
||||
|
||||
* **BLUR**
|
||||
* **CONTOUR**
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
:py:mod:`~PIL.ImageMath` Module
|
||||
===============================
|
||||
|
||||
The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”. The
|
||||
module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes
|
||||
an expression string and one or more images.
|
||||
The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that
|
||||
can take a number of images and generate a result.
|
||||
|
||||
:py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band
|
||||
images, use the :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge`
|
||||
function.
|
||||
|
||||
Example: Using the :py:mod:`~PIL.ImageMath` module
|
||||
--------------------------------------------------
|
||||
|
@ -17,35 +20,69 @@ Example: Using the :py:mod:`~PIL.ImageMath` module
|
|||
|
||||
with Image.open("image1.jpg") as im1:
|
||||
with Image.open("image2.jpg") as im2:
|
||||
out = ImageMath.lambda_eval(
|
||||
lambda args: args["convert"](args["min"](args["a"], args["b"]), 'L'),
|
||||
a=im1,
|
||||
b=im2
|
||||
)
|
||||
out = ImageMath.unsafe_eval(
|
||||
"convert(min(a, b), 'L')",
|
||||
a=im1,
|
||||
b=im2
|
||||
)
|
||||
|
||||
out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2)
|
||||
out.save("result.png")
|
||||
.. py:function:: lambda_eval(expression, options)
|
||||
|
||||
.. py:function:: eval(expression, environment)
|
||||
Returns the result of an image function.
|
||||
|
||||
Evaluate expression in the given environment.
|
||||
:param expression: A function that receives a dictionary.
|
||||
:param options: Values to add to the function's dictionary, mapping image
|
||||
names to Image instances. You can use one or more keyword
|
||||
arguments instead of a dictionary, as shown in the above
|
||||
example. Note that the names must be valid Python
|
||||
identifiers.
|
||||
:return: An image, an integer value, a floating point value,
|
||||
or a pixel tuple, depending on the expression.
|
||||
|
||||
In the current version, :py:mod:`~PIL.ImageMath` only supports
|
||||
single-layer images. To process multi-band images, use the
|
||||
:py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge`
|
||||
function.
|
||||
.. py:function:: unsafe_eval(expression, options)
|
||||
|
||||
Evaluates an image expression.
|
||||
|
||||
.. danger::
|
||||
This uses Python's ``eval()`` function to process the expression string,
|
||||
and carries the security risks of doing so. It is not
|
||||
recommended to process expressions without considering this.
|
||||
:py:meth:`lambda_eval` is a more secure alternative.
|
||||
|
||||
:py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band
|
||||
images, use the :py:meth:`~PIL.Image.Image.split` method or
|
||||
:py:func:`~PIL.Image.merge` function.
|
||||
|
||||
:param expression: A string which uses the standard Python expression
|
||||
syntax. In addition to the standard operators, you can
|
||||
also use the functions described below.
|
||||
:param environment: A dictionary that maps image names to Image instances.
|
||||
You can use one or more keyword arguments instead of a
|
||||
dictionary, as shown in the above example. Note that
|
||||
the names must be valid Python identifiers.
|
||||
:param options: Values to add to the function's dictionary, mapping image
|
||||
names to Image instances. You can use one or more keyword
|
||||
arguments instead of a dictionary, as shown in the above
|
||||
example. Note that the names must be valid Python
|
||||
identifiers.
|
||||
:return: An image, an integer value, a floating point value,
|
||||
or a pixel tuple, depending on the expression.
|
||||
|
||||
Expression syntax
|
||||
-----------------
|
||||
|
||||
Expressions are standard Python expressions, but they’re evaluated in a
|
||||
non-standard environment. You can use PIL methods as usual, plus the following
|
||||
set of operators and functions:
|
||||
* :py:meth:`lambda_eval` expressions are functions that receive a dictionary
|
||||
containing images and operators.
|
||||
|
||||
* :py:meth:`unsafe_eval` expressions are standard Python expressions,
|
||||
but they’re evaluated in a non-standard environment.
|
||||
|
||||
.. danger::
|
||||
:py:meth:`unsafe_eval` uses Python's ``eval()`` function to process the
|
||||
expression string, and carries the security risks of doing so.
|
||||
It is not recommended to process expressions without considering this.
|
||||
:py:meth:`lambda_eval` is a more secure alternative.
|
||||
|
||||
Standard Operators
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
|
|
@ -21,8 +21,8 @@ vector data. Path objects can be passed to the methods on the
|
|||
|
||||
The path object implements most parts of the Python sequence interface, and
|
||||
behaves like a list of (x, y) pairs. You can use len(), item access, and
|
||||
slicing as usual. However, the current version does not support slice
|
||||
assignment, or item and slice deletion.
|
||||
slicing as usual. However, this does not support slice assignment, or item
|
||||
and slice deletion.
|
||||
|
||||
:param xy: A sequence. The sequence can contain 2-tuples [(x, y), ...]
|
||||
or a flat list of numbers [x, y, ...].
|
||||
|
|
|
@ -7,67 +7,6 @@
|
|||
The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or
|
||||
for a region of an image.
|
||||
|
||||
.. py:class:: Stat(image_or_list, mask=None)
|
||||
|
||||
Calculate statistics for the given image. If a mask is included,
|
||||
only the regions covered by that mask are included in the
|
||||
statistics. You can also pass in a previously calculated histogram.
|
||||
|
||||
:param image: A PIL image, or a precalculated histogram.
|
||||
|
||||
.. note::
|
||||
|
||||
For a PIL image, calculations rely on the
|
||||
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
|
||||
grouped into 256 bins, even if the image has more than 8 bits per
|
||||
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
|
||||
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
|
||||
of more than 255.
|
||||
|
||||
:param mask: An optional mask.
|
||||
|
||||
.. py:attribute:: extrema
|
||||
|
||||
Min/max values for each band in the image.
|
||||
|
||||
.. note::
|
||||
|
||||
This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
|
||||
simply returns the low and high bins used. This is correct for
|
||||
images with 8 bits per channel, but fails for other modes such as
|
||||
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
|
||||
return per-band extrema for the image. This is more correct and
|
||||
efficient because, for non-8-bit modes, the histogram method uses
|
||||
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
|
||||
|
||||
.. py:attribute:: count
|
||||
|
||||
Total number of pixels for each band in the image.
|
||||
|
||||
.. py:attribute:: sum
|
||||
|
||||
Sum of all pixels for each band in the image.
|
||||
|
||||
.. py:attribute:: sum2
|
||||
|
||||
Squared sum of all pixels for each band in the image.
|
||||
|
||||
.. py:attribute:: mean
|
||||
|
||||
Average (arithmetic mean) pixel level for each band in the image.
|
||||
|
||||
.. py:attribute:: median
|
||||
|
||||
Median pixel level for each band in the image.
|
||||
|
||||
.. py:attribute:: rms
|
||||
|
||||
RMS (root-mean-square) for each band in the image.
|
||||
|
||||
.. py:attribute:: var
|
||||
|
||||
Variance for each band in the image.
|
||||
|
||||
.. py:attribute:: stddev
|
||||
|
||||
Standard deviation for each band in the image.
|
||||
.. autoclass:: Stat
|
||||
:members:
|
||||
:special-members: __init__
|
||||
|
|
|
@ -29,7 +29,7 @@ they do not extend beyond the bitmap image.
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If an attacker has control over the keys passed to the
|
||||
``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute
|
||||
``environment`` argument of :py:meth:`!PIL.ImageMath.eval`, they may be able to execute
|
||||
arbitrary code. To prevent this, keys matching the names of builtins and keys
|
||||
containing double underscores will now raise a :py:exc:`ValueError`.
|
||||
|
||||
|
|
|
@ -4,21 +4,21 @@
|
|||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
ImageMath eval()
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
.. danger::
|
||||
``ImageMath.eval()`` uses Python's ``eval()`` function to process the expression
|
||||
string, and carries the security risks of doing so. A direct replacement for this is
|
||||
the new :py:meth:`~PIL.ImageMath.unsafe_eval`, but that carries the same risks. It is
|
||||
not recommended to process expressions without considering this.
|
||||
:py:meth:`~PIL.ImageMath.lambda_eval` is a more secure alternative.
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
:cve:`2024-28219`: Fix buffer overflow in ``_imagingcms.c``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
In ``_imagingcms.c``, two ``strcpy`` calls were able to copy too much data into fixed
|
||||
length strings. This has been fixed by using ``strncpy`` instead.
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
@ -58,13 +58,34 @@ Deprecated Use instead
|
|||
:py:data:`sys.version_info`, and ``PIL.__version__``
|
||||
============================================ ====================================================
|
||||
|
||||
ImageMath.eval()
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or
|
||||
:py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more
|
||||
information.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
Added alpha_quality argument when saving WebP images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
When saving WebP images, an ``alpha_quality`` argument can be passed to the encoder. It
|
||||
is an integer value between 0 to 100, where values other than 100 will provide lossy
|
||||
compression.
|
||||
|
||||
Negative kmeans error
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When calling :py:meth:`~PIL.Image.Image.quantize`, a negative ``kmeans`` will now
|
||||
raise a :py:exc:`ValueError`, unless a palette is supplied to make the value redundant.
|
||||
|
||||
Negative P1-P3 PPM value error
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If a P1-P3 PPM image contains a negative value, a :py:exc:`ValueError` will now be
|
||||
raised.
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
@ -90,3 +111,9 @@ Release GIL when fetching WebP frames
|
|||
|
||||
Python's Global Interpreter Lock is now released when fetching WebP frames from
|
||||
the libwebp decoder.
|
||||
|
||||
Type hints
|
||||
^^^^^^^^^^
|
||||
|
||||
Pillow now has type hints for a large part of its modules, and the package
|
||||
includes a ``py.typed`` file and the ``Typing :: Typed`` Trove classifier.
|
||||
|
|
59
docs/releasenotes/10.4.0.rst
Normal file
59
docs/releasenotes/10.4.0.rst
Normal file
|
@ -0,0 +1,59 @@
|
|||
10.4.0
|
||||
------
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
BGR;15, BGR 16 and BGR;24
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
|
||||
|
||||
Support for LibTIFF earlier than 4
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||
Upgrade to a newer version of LibTIFF instead.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
|
@ -47,7 +47,7 @@ Google's `OSS-Fuzz`_ project for finding this issue.
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To limit :py:class:`PIL.ImageMath` to working with images, Pillow
|
||||
will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will
|
||||
will now restrict the builtins available to :py:meth:`!PIL.ImageMath.eval`. This will
|
||||
help prevent problems arising if users evaluate arbitrary expressions, such as
|
||||
``ImageMath.eval("exec(exit())")``.
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ has been present since PIL.
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
While Pillow 9.0 restricted top-level builtins available to
|
||||
:py:meth:`PIL.ImageMath.eval`, it did not prevent builtins
|
||||
:py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins
|
||||
available to lambda expressions. These are now also restricted.
|
||||
|
||||
Other Changes
|
||||
|
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
10.4.0
|
||||
10.3.0
|
||||
10.2.0
|
||||
10.1.0
|
||||
|
|
|
@ -15,7 +15,7 @@ keywords = [
|
|||
"Imaging",
|
||||
]
|
||||
license = {text = "HPND"}
|
||||
authors = [{name = "Jeffrey A. Clark (Alex)", email = "aclark@aclark.net"}]
|
||||
authors = [{name = "Jeffrey A. Clark", email = "aclark@aclark.net"}]
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
|
@ -42,10 +42,9 @@ dynamic = [
|
|||
docs = [
|
||||
"furo",
|
||||
"olefile",
|
||||
"sphinx>=2.4",
|
||||
"sphinx>=7.3",
|
||||
"sphinx-copybutton",
|
||||
"sphinx-inline-tabs",
|
||||
"sphinx-removed-in",
|
||||
"sphinxext-opengraph",
|
||||
]
|
||||
fpx = [
|
||||
|
@ -96,6 +95,9 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
|
|||
test-command = "cd {project} && .github/workflows/wheels-test.sh"
|
||||
test-extras = "tests"
|
||||
|
||||
[tool.ruff]
|
||||
fix = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"C4", # flake8-comprehensions
|
||||
|
@ -106,6 +108,7 @@ select = [
|
|||
"ISC", # flake8-implicit-str-concat
|
||||
"LOG", # flake8-logging
|
||||
"PGH", # pygrep-hooks
|
||||
"PYI", # flake8-pyi
|
||||
"RUF100", # unused noqa (yesqa)
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle warnings
|
||||
|
@ -116,6 +119,8 @@ ignore = [
|
|||
"E221", # Multiple spaces before operator
|
||||
"E226", # Missing whitespace around arithmetic operator
|
||||
"E241", # Multiple spaces after ','
|
||||
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
|
||||
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
|
|
|
@ -139,7 +139,9 @@ def testimage() -> None:
|
|||
In 1.1.6, you can use the ImageMath module to do image
|
||||
calculations.
|
||||
|
||||
>>> im = ImageMath.eval("float(im + 20)", im=im.convert("L"))
|
||||
>>> im = ImageMath.lambda_eval( \
|
||||
lambda args: args["float"](args["im"] + 20), im=im.convert("L") \
|
||||
)
|
||||
>>> im.mode, im.size
|
||||
('F', (128, 128))
|
||||
|
||||
|
@ -163,9 +165,9 @@ if __name__ == "__main__":
|
|||
print("Running selftest:")
|
||||
status = doctest.testmod(sys.modules[__name__])
|
||||
if status[0]:
|
||||
print("*** %s tests of %d failed." % status)
|
||||
print(f"*** {status[0]} tests of {status[1]} failed.")
|
||||
exit_status = 1
|
||||
else:
|
||||
print("--- %s tests passed." % status[1])
|
||||
print(f"--- {status[1]} tests passed.")
|
||||
|
||||
sys.exit(exit_status)
|
||||
|
|
31
setup.py
31
setup.py
|
@ -23,8 +23,10 @@ from setuptools.command.build_ext import build_ext
|
|||
def get_version():
|
||||
version_file = "src/PIL/_version.py"
|
||||
with open(version_file, encoding="utf-8") as f:
|
||||
exec(compile(f.read(), version_file, "exec"))
|
||||
return locals()["__version__"]
|
||||
return f.read().split('"')[1]
|
||||
|
||||
|
||||
configuration = {}
|
||||
|
||||
|
||||
PILLOW_VERSION = get_version()
|
||||
|
@ -334,15 +336,24 @@ class pil_build_ext(build_ext):
|
|||
+ [("add-imaging-libs=", None, "Add libs to _imaging build")]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def check_configuration(option, value):
|
||||
return True if value in configuration.get(option, []) else None
|
||||
|
||||
def initialize_options(self):
|
||||
self.disable_platform_guessing = None
|
||||
self.disable_platform_guessing = self.check_configuration(
|
||||
"platform-guessing", "disable"
|
||||
)
|
||||
self.add_imaging_libs = ""
|
||||
build_ext.initialize_options(self)
|
||||
for x in self.feature:
|
||||
setattr(self, f"disable_{x}", None)
|
||||
setattr(self, f"enable_{x}", None)
|
||||
setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
|
||||
setattr(self, f"enable_{x}", self.check_configuration(x, "enable"))
|
||||
for x in ("raqm", "fribidi"):
|
||||
setattr(self, f"vendor_{x}", None)
|
||||
setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor"))
|
||||
if self.check_configuration("debug", "true"):
|
||||
self.debug = True
|
||||
self.parallel = configuration.get("parallel", [None])[-1]
|
||||
|
||||
def finalize_options(self):
|
||||
build_ext.finalize_options(self)
|
||||
|
@ -1007,6 +1018,12 @@ ext_modules = [
|
|||
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
|
||||
]
|
||||
|
||||
|
||||
# parse configuration from _custom_build/backend.py
|
||||
while sys.argv[-1].startswith("--pillow-configuration="):
|
||||
_, key, value = sys.argv.pop().split("=", 2)
|
||||
configuration.setdefault(key, []).append(value)
|
||||
|
||||
try:
|
||||
setup(
|
||||
cmdclass={"build_ext": pil_build_ext},
|
||||
|
@ -1020,7 +1037,7 @@ The headers or library files could not be found for {str(err)},
|
|||
a required dependency when compiling Pillow from source.
|
||||
|
||||
Please see the install instructions at:
|
||||
https://pillow.readthedocs.io/en/latest/installation.html
|
||||
https://pillow.readthedocs.io/en/latest/installation/basic-installation.html
|
||||
|
||||
"""
|
||||
sys.stderr.write(msg)
|
||||
|
|
|
@ -241,7 +241,7 @@ class BLPFormatError(NotImplementedError):
|
|||
pass
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] in (b"BLP1", b"BLP2")
|
||||
|
||||
|
||||
|
@ -253,7 +253,7 @@ class BlpImageFile(ImageFile.ImageFile):
|
|||
format = "BLP"
|
||||
format_description = "Blizzard Mipmap Format"
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
self.magic = self.fp.read(4)
|
||||
|
||||
self.fp.seek(5, os.SEEK_CUR)
|
||||
|
@ -333,7 +333,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
|
|||
|
||||
|
||||
class BLP1Decoder(_BLPBaseDecoder):
|
||||
def _load(self):
|
||||
def _load(self) -> None:
|
||||
if self._blp_compression == Format.JPEG:
|
||||
self._decode_jpeg_stream()
|
||||
|
||||
|
@ -341,7 +341,7 @@ class BLP1Decoder(_BLPBaseDecoder):
|
|||
if self._blp_encoding in (4, 5):
|
||||
palette = self._read_palette()
|
||||
data = self._read_bgra(palette)
|
||||
self.set_as_raw(bytes(data))
|
||||
self.set_as_raw(data)
|
||||
else:
|
||||
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
|
||||
raise BLPFormatError(msg)
|
||||
|
@ -412,13 +412,13 @@ class BLP2Decoder(_BLPBaseDecoder):
|
|||
msg = f"Unknown BLP compression {repr(self._blp_compression)}"
|
||||
raise BLPFormatError(msg)
|
||||
|
||||
self.set_as_raw(bytes(data))
|
||||
self.set_as_raw(data)
|
||||
|
||||
|
||||
class BLPEncoder(ImageFile.PyEncoder):
|
||||
_pushes_fd = True
|
||||
|
||||
def _write_palette(self):
|
||||
def _write_palette(self) -> bytes:
|
||||
data = b""
|
||||
palette = self.im.getpalette("RGBA", "RGBA")
|
||||
for i in range(len(palette) // 4):
|
||||
|
|
|
@ -48,12 +48,12 @@ BIT2MODE = {
|
|||
}
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:2] == b"BM"
|
||||
|
||||
|
||||
def _dib_accept(prefix):
|
||||
return i32(prefix) in [12, 40, 64, 108, 124]
|
||||
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
@ -83,8 +83,9 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
# read the rest of the bmp header, without its size
|
||||
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
|
||||
|
||||
# -------------------------------------------------- IBM OS/2 Bitmap v1
|
||||
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
|
||||
# ----- This format has different offsets because of width/height types
|
||||
# 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
|
||||
if file_info["header_size"] == 12:
|
||||
file_info["width"] = i16(header_data, 0)
|
||||
file_info["height"] = i16(header_data, 2)
|
||||
|
@ -93,9 +94,14 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
file_info["compression"] = self.RAW
|
||||
file_info["palette_padding"] = 3
|
||||
|
||||
# --------------------------------------------- Windows Bitmap v2 to v5
|
||||
# v3, OS/2 v2, v4, v5
|
||||
elif file_info["header_size"] in (40, 64, 108, 124):
|
||||
# --------------------------------------------- Windows Bitmap v3 to v5
|
||||
# 40: BITMAPINFOHEADER
|
||||
# 52: BITMAPV2HEADER
|
||||
# 56: BITMAPV3HEADER
|
||||
# 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER
|
||||
# 108: BITMAPV4HEADER
|
||||
# 124: BITMAPV5HEADER
|
||||
elif file_info["header_size"] in (40, 52, 56, 64, 108, 124):
|
||||
file_info["y_flip"] = header_data[7] == 0xFF
|
||||
file_info["direction"] = 1 if file_info["y_flip"] else -1
|
||||
file_info["width"] = i32(header_data, 0)
|
||||
|
@ -117,10 +123,13 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
file_info["palette_padding"] = 4
|
||||
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
|
||||
if file_info["compression"] == self.BITFIELDS:
|
||||
if len(header_data) >= 52:
|
||||
for idx, mask in enumerate(
|
||||
["r_mask", "g_mask", "b_mask", "a_mask"]
|
||||
):
|
||||
masks = ["r_mask", "g_mask", "b_mask"]
|
||||
if len(header_data) >= 48:
|
||||
if len(header_data) >= 52:
|
||||
masks.append("a_mask")
|
||||
else:
|
||||
file_info["a_mask"] = 0x0
|
||||
for idx, mask in enumerate(masks):
|
||||
file_info[mask] = i32(header_data, 36 + idx * 4)
|
||||
else:
|
||||
# 40 byte headers only have the three components in the
|
||||
|
@ -132,7 +141,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
# location, but it is listed as a reserved component,
|
||||
# and it is not generally an alpha channel
|
||||
file_info["a_mask"] = 0x0
|
||||
for mask in ["r_mask", "g_mask", "b_mask"]:
|
||||
for mask in masks:
|
||||
file_info[mask] = i32(read(4))
|
||||
file_info["rgb_mask"] = (
|
||||
file_info["r_mask"],
|
||||
|
@ -175,9 +184,11 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
32: [
|
||||
(0xFF0000, 0xFF00, 0xFF, 0x0),
|
||||
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
|
||||
(0xFF000000, 0xFF00, 0xFF, 0x0),
|
||||
(0xFF000000, 0xFF0000, 0xFF00, 0xFF),
|
||||
(0xFF, 0xFF00, 0xFF0000, 0xFF000000),
|
||||
(0xFF0000, 0xFF00, 0xFF, 0xFF000000),
|
||||
(0xFF000000, 0xFF00, 0xFF, 0xFF0000),
|
||||
(0x0, 0x0, 0x0, 0x0),
|
||||
],
|
||||
24: [(0xFF0000, 0xFF00, 0xFF)],
|
||||
|
@ -186,9 +197,11 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
MASK_MODES = {
|
||||
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
|
||||
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
|
||||
(32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
|
||||
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
|
||||
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
|
||||
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
|
||||
(32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
|
||||
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
|
||||
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
|
||||
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
|
||||
|
@ -270,7 +283,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
)
|
||||
]
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
"""Open file, check magic number and read header"""
|
||||
# read 14 bytes: magic number, filesize, reserved, header final offset
|
||||
head_data = self.fp.read(14)
|
||||
|
@ -291,7 +304,8 @@ class BmpRleDecoder(ImageFile.PyDecoder):
|
|||
rle4 = self.args[1]
|
||||
data = bytearray()
|
||||
x = 0
|
||||
while len(data) < self.state.xsize * self.state.ysize:
|
||||
dest_length = self.state.xsize * self.state.ysize
|
||||
while len(data) < dest_length:
|
||||
pixels = self.fd.read(1)
|
||||
byte = self.fd.read(1)
|
||||
if not pixels or not byte:
|
||||
|
@ -362,7 +376,7 @@ class DibImageFile(BmpImageFile):
|
|||
format = "DIB"
|
||||
format_description = "Windows Bitmap"
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
self._bitmap()
|
||||
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ def register_handler(handler):
|
|||
# Image adapter
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
|
||||
|
||||
|
||||
|
@ -37,7 +37,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
|
|||
format = "BUFR"
|
||||
format_description = "BUFR"
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
offset = self.fp.tell()
|
||||
|
||||
if not _accept(self.fp.read(4)):
|
||||
|
|
|
@ -25,7 +25,7 @@ from ._binary import i32le as i32
|
|||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] == b"\0\0\2\0"
|
||||
|
||||
|
||||
|
@ -37,7 +37,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
format = "CUR"
|
||||
format_description = "Windows Cursor"
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
offset = self.fp.tell()
|
||||
|
||||
# check magic
|
||||
|
|
|
@ -29,7 +29,7 @@ from .PcxImagePlugin import PcxImageFile
|
|||
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return len(prefix) >= 4 and i32(prefix) == MAGIC
|
||||
|
||||
|
||||
|
@ -63,7 +63,7 @@ class DcxImageFile(PcxImageFile):
|
|||
self.is_animated = self.n_frames > 1
|
||||
self.seek(0)
|
||||
|
||||
def seek(self, frame):
|
||||
def seek(self, frame: int) -> None:
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
self.frame = frame
|
||||
|
@ -71,7 +71,7 @@ class DcxImageFile(PcxImageFile):
|
|||
self.fp.seek(self._offset[frame])
|
||||
PcxImageFile._open(self)
|
||||
|
||||
def tell(self):
|
||||
def tell(self) -> int:
|
||||
return self.frame
|
||||
|
||||
|
||||
|
|
|
@ -271,16 +271,16 @@ class D3DFMT(IntEnum):
|
|||
module = sys.modules[__name__]
|
||||
for item in DDSD:
|
||||
assert item.name is not None
|
||||
setattr(module, "DDSD_" + item.name, item.value)
|
||||
setattr(module, f"DDSD_{item.name}", item.value)
|
||||
for item1 in DDSCAPS:
|
||||
assert item1.name is not None
|
||||
setattr(module, "DDSCAPS_" + item1.name, item1.value)
|
||||
setattr(module, f"DDSCAPS_{item1.name}", item1.value)
|
||||
for item2 in DDSCAPS2:
|
||||
assert item2.name is not None
|
||||
setattr(module, "DDSCAPS2_" + item2.name, item2.value)
|
||||
setattr(module, f"DDSCAPS2_{item2.name}", item2.value)
|
||||
for item3 in DDPF:
|
||||
assert item3.name is not None
|
||||
setattr(module, "DDPF_" + item3.name, item3.value)
|
||||
setattr(module, f"DDPF_{item3.name}", item3.value)
|
||||
|
||||
DDS_FOURCC = DDPF.FOURCC
|
||||
DDS_RGB = DDPF.RGB
|
||||
|
@ -331,7 +331,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
format = "DDS"
|
||||
format_description = "DirectDraw Surface"
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
if not _accept(self.fp.read(4)):
|
||||
msg = "not a DDS file"
|
||||
raise SyntaxError(msg)
|
||||
|
@ -472,7 +472,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
else:
|
||||
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
|
||||
|
||||
def load_seek(self, pos):
|
||||
def load_seek(self, pos: int) -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
@ -497,7 +497,8 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
|
|||
|
||||
data = bytearray()
|
||||
bytecount = bitcount // 8
|
||||
while len(data) < self.state.xsize * self.state.ysize * len(masks):
|
||||
dest_length = self.state.xsize * self.state.ysize * len(masks)
|
||||
while len(data) < dest_length:
|
||||
value = int.from_bytes(self.fd.read(bytecount), "little")
|
||||
for i, mask in enumerate(masks):
|
||||
masked_value = value & mask
|
||||
|
@ -505,7 +506,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
|
|||
data += o8(
|
||||
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
|
||||
)
|
||||
self.set_as_raw(bytes(data))
|
||||
self.set_as_raw(data)
|
||||
return -1, 0
|
||||
|
||||
|
||||
|
@ -561,7 +562,7 @@ def _save(im, fp, filename):
|
|||
)
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] == b"DDS "
|
||||
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ gs_binary: str | bool | None = None
|
|||
gs_windows_binary = None
|
||||
|
||||
|
||||
def has_ghostscript():
|
||||
def has_ghostscript() -> bool:
|
||||
global gs_binary, gs_windows_binary
|
||||
if gs_binary is None:
|
||||
if sys.platform.startswith("win"):
|
||||
|
@ -178,7 +178,7 @@ class PSFile:
|
|||
self.char = None
|
||||
self.fp.seek(offset, whence)
|
||||
|
||||
def readline(self):
|
||||
def readline(self) -> str:
|
||||
s = [self.char or b""]
|
||||
self.char = None
|
||||
|
||||
|
@ -195,7 +195,7 @@ class PSFile:
|
|||
return b"".join(s).decode("latin-1")
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
|
||||
|
||||
|
||||
|
@ -212,7 +212,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
|
||||
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
|
||||
|
||||
def _open(self):
|
||||
def _open(self) -> None:
|
||||
(length, offset) = self._find_offset(self.fp)
|
||||
|
||||
# go to offset - start of "%!PS"
|
||||
|
@ -404,7 +404,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
self.tile = []
|
||||
return Image.Image.load(self)
|
||||
|
||||
def load_seek(self, pos):
|
||||
def load_seek(self, pos: int) -> None:
|
||||
# we can't incrementally load, so force ImageFile.parser to
|
||||
# use our custom load method by defining this method.
|
||||
pass
|
||||
|
|
|
@ -346,7 +346,7 @@ class Interop(IntEnum):
|
|||
InteropVersion = 2
|
||||
RelatedImageFileFormat = 4096
|
||||
RelatedImageWidth = 4097
|
||||
RleatedImageHeight = 4098
|
||||
RelatedImageHeight = 4098
|
||||
|
||||
|
||||
class IFD(IntEnum):
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import math
|
||||
|
||||
from . import Image, ImageFile
|
||||
|
@ -27,14 +28,32 @@ class FitsImageFile(ImageFile.ImageFile):
|
|||
assert self.fp is not None
|
||||
|
||||
headers: dict[bytes, bytes] = {}
|
||||
header_in_progress = False
|
||||
decoder_name = ""
|
||||
while True:
|
||||
header = self.fp.read(80)
|
||||
if not header:
|
||||
msg = "Truncated FITS file"
|
||||
raise OSError(msg)
|
||||
keyword = header[:8].strip()
|
||||
if keyword == b"END":
|
||||
if keyword in (b"SIMPLE", b"XTENSION"):
|
||||
header_in_progress = True
|
||||
elif headers and not header_in_progress:
|
||||
# This is now a data unit
|
||||
break
|
||||
elif keyword == b"END":
|
||||
# Seek to the end of the header unit
|
||||
self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880)
|
||||
if not decoder_name:
|
||||
decoder_name, offset, args = self._parse_headers(headers)
|
||||
|
||||
header_in_progress = False
|
||||
continue
|
||||
|
||||
if decoder_name:
|
||||
# Keep going to read past the headers
|
||||
continue
|
||||
|
||||
value = header[8:].split(b"/")[0].strip()
|
||||
if value.startswith(b"="):
|
||||
value = value[1:].strip()
|
||||
|
@ -43,32 +62,87 @@ class FitsImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError(msg)
|
||||
headers[keyword] = value
|
||||
|
||||
naxis = int(headers[b"NAXIS"])
|
||||
if naxis == 0:
|
||||
if not decoder_name:
|
||||
msg = "No image data"
|
||||
raise ValueError(msg)
|
||||
elif naxis == 1:
|
||||
self._size = 1, int(headers[b"NAXIS1"])
|
||||
else:
|
||||
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
|
||||
|
||||
number_of_bits = int(headers[b"BITPIX"])
|
||||
offset += self.fp.tell() - 80
|
||||
self.tile = [(decoder_name, (0, 0) + self.size, offset, args)]
|
||||
|
||||
def _get_size(
|
||||
self, headers: dict[bytes, bytes], prefix: bytes
|
||||
) -> tuple[int, int] | None:
|
||||
naxis = int(headers[prefix + b"NAXIS"])
|
||||
if naxis == 0:
|
||||
return None
|
||||
|
||||
if naxis == 1:
|
||||
return 1, int(headers[prefix + b"NAXIS1"])
|
||||
else:
|
||||
return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"])
|
||||
|
||||
def _parse_headers(
|
||||
self, headers: dict[bytes, bytes]
|
||||
) -> tuple[str, int, tuple[str | int, ...]]:
|
||||
prefix = b""
|
||||
decoder_name = "raw"
|
||||
offset = 0
|
||||
if (
|
||||
headers.get(b"XTENSION") == b"'BINTABLE'"
|
||||
and headers.get(b"ZIMAGE") == b"T"
|
||||
and headers[b"ZCMPTYPE"] == b"'GZIP_1 '"
|
||||
):
|
||||
no_prefix_size = self._get_size(headers, prefix) or (0, 0)
|
||||
number_of_bits = int(headers[b"BITPIX"])
|
||||
offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8)
|
||||
|
||||
prefix = b"Z"
|
||||
decoder_name = "fits_gzip"
|
||||
|
||||
size = self._get_size(headers, prefix)
|
||||
if not size:
|
||||
return "", 0, ()
|
||||
|
||||
self._size = size
|
||||
|
||||
number_of_bits = int(headers[prefix + b"BITPIX"])
|
||||
if number_of_bits == 8:
|
||||
self._mode = "L"
|
||||
elif number_of_bits == 16:
|
||||
self._mode = "I"
|
||||
self._mode = "I;16"
|
||||
elif number_of_bits == 32:
|
||||
self._mode = "I"
|
||||
elif number_of_bits in (-32, -64):
|
||||
self._mode = "F"
|
||||
|
||||
offset = math.ceil(self.fp.tell() / 2880) * 2880
|
||||
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
|
||||
args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,)
|
||||
return decoder_name, offset, args
|
||||
|
||||
|
||||
class FitsGzipDecoder(ImageFile.PyDecoder):
|
||||
_pulls_fd = True
|
||||
|
||||
def decode(self, buffer):
|
||||
assert self.fd is not None
|
||||
value = gzip.decompress(self.fd.read())
|
||||
|
||||
rows = []
|
||||
offset = 0
|
||||
number_of_bits = min(self.args[0] // 8, 4)
|
||||
for y in range(self.state.ysize):
|
||||
row = bytearray()
|
||||
for x in range(self.state.xsize):
|
||||
row += value[offset + (4 - number_of_bits) : offset + 4]
|
||||
offset += 4
|
||||
rows.append(row)
|
||||
self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row]))
|
||||
return -1, 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Registry
|
||||
|
||||
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
|
||||
Image.register_decoder("fits_gzip", FitsGzipDecoder)
|
||||
|
||||
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])
|
||||
|
|
|
@ -27,7 +27,7 @@ from ._binary import o8
|
|||
# decoder
|
||||
|
||||
|
||||
def _accept(prefix):
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return (
|
||||
len(prefix) >= 6
|
||||
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||
|
@ -123,7 +123,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
palette[i] = (r, g, b)
|
||||
i += 1
|
||||
|
||||
def seek(self, frame):
|
||||
def seek(self, frame: int) -> None:
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
if frame < self.__frame:
|
||||
|
@ -132,7 +132,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
for f in range(self.__frame + 1, frame + 1):
|
||||
self._seek(f)
|
||||
|
||||
def _seek(self, frame):
|
||||
def _seek(self, frame: int) -> None:
|
||||
if frame == 0:
|
||||
self.__frame = -1
|
||||
self._fp.seek(self.__rewind)
|
||||
|
@ -162,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
|
||||
self.__offset += framesize
|
||||
|
||||
def tell(self):
|
||||
def tell(self) -> int:
|
||||
return self.__frame
|
||||
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user