Merge branch 'main' into jxl-support2

This commit is contained in:
Andrew Murray 2024-05-22 20:08:39 +10:00 committed by GitHub
commit 9313587fec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
197 changed files with 4007 additions and 1962 deletions

View File

@ -1,3 +1,10 @@
skip_commits:
files:
- ".github/**/*"
- ".gitmodules"
- "docs/**/*"
- "wheels/**/*"
version: '{build}' version: '{build}'
clone_folder: c:\pillow clone_folder: c:\pillow
init: init:
@ -27,7 +34,7 @@ install:
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - 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 - 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:\ - 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% - path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |

View File

@ -1 +1 @@
cibuildwheel==2.17.0 cibuildwheel==2.18.1

View File

@ -1 +1 @@
mypy==1.9.0 mypy==1.10.0

View File

@ -9,6 +9,7 @@ BinPackParameters: false
BreakBeforeBraces: Attach BreakBeforeBraces: Attach
ColumnLimit: 88 ColumnLimit: 88
DerivePointerAlignment: false DerivePointerAlignment: false
IndentGotoLabels: false
IndentWidth: 4 IndentWidth: 4
Language: Cpp Language: Cpp
PointerAlignment: Right PointerAlignment: Right

View File

@ -48,6 +48,21 @@ Thank you.
* Python: * Python:
* Pillow: * 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. 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.

View File

@ -55,6 +55,7 @@ jobs:
packages: > packages: >
gcc-g++ gcc-g++
ghostscript ghostscript
git
ImageMagick ImageMagick
jpeg jpeg
libfreetype-devel libfreetype-devel
@ -132,11 +133,12 @@ jobs:
bash.exe .ci/after_success.sh bash.exe .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Cygwin flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }} name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -36,32 +36,31 @@ jobs:
docker: [ docker: [
# Run slower jobs first to give them a headstart and reduce waiting time # Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-22.04-jammy-arm64v8, ubuntu-22.04-jammy-arm64v8,
ubuntu-22.04-jammy-ppc64le, ubuntu-24.04-noble-ppc64le,
ubuntu-22.04-jammy-s390x, ubuntu-24.04-noble-s390x,
# Then run the remainder # Then run the remainder
alpine, alpine,
amazon-2-amd64, amazon-2-amd64,
amazon-2023-amd64, amazon-2023-amd64,
arch, arch,
centos-7-amd64,
centos-stream-8-amd64,
centos-stream-9-amd64, centos-stream-9-amd64,
debian-11-bullseye-amd64, debian-11-bullseye-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-38-amd64,
fedora-39-amd64, fedora-39-amd64,
fedora-40-amd64,
gentoo, gentoo,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64,
] ]
dockerTag: [main] dockerTag: [main]
include: include:
- docker: "ubuntu-22.04-jammy-arm64v8" - docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64" qemu-arch: "aarch64"
- docker: "ubuntu-22.04-jammy-ppc64le" - docker: "ubuntu-24.04-noble-ppc64le"
qemu-arch: "ppc64le" qemu-arch: "ppc64le"
- docker: "ubuntu-22.04-jammy-s390x" - docker: "ubuntu-24.04-noble-s390x"
qemu-arch: "s390x" qemu-arch: "s390x"
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
@ -83,8 +82,8 @@ jobs:
- name: Docker build - name: Docker build
run: | run: |
# The Pillow user in the docker container is UID 1000 # The Pillow user in the docker container is UID 1001
sudo chown -R 1000 $GITHUB_WORKSPACE sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE sudo chown -R runner $GITHUB_WORKSPACE
@ -101,11 +100,12 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }} MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
gcov: true gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -85,8 +85,9 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: "MSYS2 MinGW" name: "MSYS2 MinGW"
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -50,7 +50,7 @@ jobs:
- name: Build and Run Valgrind - name: Build and Run Valgrind
run: | run: |
# The Pillow user in the docker container is UID 1000 # The Pillow user in the docker container is UID 1001
sudo chown -R 1000 $GITHUB_WORKSPACE 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 }} 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 sudo chown -R runner $GITHUB_WORKSPACE

View File

@ -86,7 +86,7 @@ jobs:
choco install nasm --no-progress choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH 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 echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images
@ -213,11 +213,12 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }} name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -57,9 +57,9 @@ jobs:
- python-version: "3.10" - python-version: "3.10"
PYTHONOPTIMIZE: 2 PYTHONOPTIMIZE: 2
# M1 only available for 3.10+ # M1 only available for 3.10+
- os: "macos-latest" - os: "macos-13"
python-version: "3.9" python-version: "3.9"
- os: "macos-latest" - os: "macos-13"
python-version: "3.8" python-version: "3.8"
exclude: exclude:
- os: "macos-14" - os: "macos-14"
@ -150,11 +150,12 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -16,7 +16,7 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.2 FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.3.0 HARFBUZZ_VERSION=8.4.0
LIBPNG_VERSION=1.6.43 LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.2 JPEGTURBO_VERSION=3.0.2
OPENJPEG_VERSION=2.5.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 build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then 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 libXau 1.0.11 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
if [[ "$CIBW_ARCHS" == "arm64" ]]; then if [[ "$CIBW_ARCHS" == "arm64" ]]; then

View File

@ -5,6 +5,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake" - "winbuild/fribidi.cmake"
@ -14,6 +15,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake" - "winbuild/fribidi.cmake"
@ -95,7 +97,7 @@ jobs:
matrix: matrix:
include: include:
- name: "macOS x86_64" - name: "macOS x86_64"
os: macos-latest os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS arm64" - name: "macOS arm64"

View File

@ -1,35 +1,42 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0 rev: v0.4.3
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.1.1 rev: 24.4.2
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.7 rev: 1.7.8
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
files: ^src/ files: ^src/
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4 rev: v1.5.5
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.4
hooks:
- id: clang-format
types: [c]
exclude: ^src/thirdparty/
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0 rev: v1.10.0
hooks: hooks:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v4.6.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
@ -42,13 +49,20 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ 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 - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.9.1 rev: v0.9.1
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.7.0 rev: 1.8.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
@ -62,5 +76,10 @@ repos:
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
ci: ci:
autoupdate_schedule: monthly autoupdate_schedule: monthly

View File

@ -6,6 +6,10 @@ build:
os: ubuntu-22.04 os: ubuntu-22.04
tools: tools:
python: "3" python: "3"
jobs:
post_checkout:
- git remote add upstream https://github.com/python-pillow/Pillow.git # For forks
- git fetch upstream --tags
python: python:
install: install:

View File

@ -2,9 +2,84 @@
Changelog (Pillow) 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 - Fixed joined corners for ImageDraw rounded_rectangle() non-integer dimensions #7881
[radarhere] [radarhere]
@ -4298,7 +4373,7 @@ Changelog (Pillow)
- Documentation changes, URL update, transpose, release checklist - Documentation changes, URL update, transpose, release checklist
[radarhere] [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] [radarhere]
- Round Image.crop arguments to the nearest integer #1745 (fixes #1744) - 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). - A handbook is available (distributed separately).
- The coordinate system is changed so that (0,0) is now located - 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. and 90% of all other image processing and graphics libraries.
- Modes "1" (bilevel) and "P" (palette) have been introduced. Note - Modes "1" (bilevel) and "P" (palette) have been introduced. Note

View File

@ -1,11 +1,11 @@
The Python Imaging Library (PIL) is The Python Imaging Library (PIL) is
Copyright © 1997-2011 by Secret Labs AB 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 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: Like PIL, Pillow is licensed under the open source HPND License:

View File

@ -2,7 +2,6 @@
.PHONY: clean .PHONY: clean
clean: clean:
python3 setup.py clean
rm src/PIL/*.so || true rm src/PIL/*.so || true
rm -r build || true rm -r build || true
find . -name __pycache__ | xargs rm -r || true find . -name __pycache__ | xargs rm -r || true
@ -78,8 +77,6 @@ release-test:
python3 selftest.py python3 selftest.py
python3 -m pytest Tests python3 -m pytest Tests
python3 -m pip install . python3 -m pip install .
-rm dist/*.egg
-rmdir dist
python3 -m pytest -qq python3 -m pytest -qq
python3 -m check_manifest python3 -m check_manifest
python3 -m pyroma . python3 -m pyroma .

View File

@ -6,9 +6,9 @@
## Python Imaging Library (Fork) ## 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). 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 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). [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 ## More Information
- [Documentation](https://pillow.readthedocs.io/) - [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) - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) - [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
- [Issues](https://github.com/python-pillow/Pillow/issues) - [Issues](https://github.com/python-pillow/Pillow/issues)

View File

@ -20,8 +20,10 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0 git tag 5.2.0
git push --tags 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) * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) 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/), * [ ] 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: increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
```bash ```bash
@ -50,7 +52,9 @@ Released as needed for security, installation or critical bug fixes.
```bash ```bash
make sdist 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: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push git push
@ -72,18 +76,14 @@ Released as needed privately to individual vendors for critical security-related
git tag 2.5.3 git tag 2.5.3
git push origin --tags 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: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push origin 2.5.x 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 ## Publicize Release
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321

View File

@ -32,9 +32,8 @@ def timer(func, label, *args) -> None:
break break
endtime = time.time() endtime = time.time()
print( print(
"{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format( f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0) f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
)
) )

View File

@ -11,6 +11,7 @@ import subprocess
import sys import sys
import sysconfig import sysconfig
import tempfile import tempfile
from functools import lru_cache
from io import BytesIO from io import BytesIO
from typing import Any, Callable, Sequence from typing import Any, Callable, Sequence
@ -114,7 +115,9 @@ def assert_image_similar(
diff = 0 diff = 0
for ach, bch in zip(a.split(), b.split()): 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())) diff += sum(i * num for i, num in enumerate(chdiff.histogram()))
ave_diff = diff / (a.size[0] * a.size[1]) 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() 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: if mode is None:
# Always return fresh not-yet-loaded version of image. # Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors # Operations on not-yet-loaded images are a separate class of errors
# what we should catch. # that we should catch.
return Image.open("Tests/images/hopper.ppm") 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 return _cached_hopper(mode).copy()
# (for fast, isolated, repeatable tests).
im = cache.get(mode)
if im is None: @lru_cache
if mode == "F": def _cached_hopper(mode: str) -> Image.Image:
im = hopper("L").convert(mode) if mode == "F":
elif mode[:4] == "I;16": im = hopper("L")
im = hopper("I").convert(mode) else:
else: im = hopper()
im = hopper().convert(mode) if mode.startswith("BGR;"):
cache[mode] = im with pytest.warns(DeprecationWarning):
return im.copy() 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: def djpeg_available() -> bool:

BIN
Tests/icc/sGrey-v2-nano.icc Normal file

Binary file not shown.

BIN
Tests/images/9bit.j2k Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
Tests/images/m13.fits Normal file

Binary file not shown.

366
Tests/images/m13_gzip.fits Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -44,6 +44,9 @@ def test_questionable() -> None:
"pal8os2sp.bmp", "pal8os2sp.bmp",
"pal8rletrns.bmp", "pal8rletrns.bmp",
"rgb32bf-xbgr.bmp", "rgb32bf-xbgr.bmp",
"rgba32.bmp",
"rgb32h52.bmp",
"rgba32h56.bmp",
] ]
for f in get_files("q"): for f in get_files("q"):
try: try:

View File

@ -37,6 +37,8 @@ def test_version() -> None:
else: else:
assert function(name) == version assert function(name) == version
if name != "PIL": 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) assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules: for module in features.modules:
@ -117,9 +119,10 @@ def test_unsupported_module() -> None:
features.version_module(module) features.version_module(module)
def test_pilinfo() -> None: @pytest.mark.parametrize("supported_formats", (True, False))
def test_pilinfo(supported_formats) -> None:
buf = io.StringIO() buf = io.StringIO()
features.pilinfo(buf) features.pilinfo(buf, supported_formats=supported_formats)
out = buf.getvalue() out = buf.getvalue()
lines = out.splitlines() lines = out.splitlines()
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
@ -129,9 +132,15 @@ def test_pilinfo() -> None:
while lines[0].startswith(" "): while lines[0].startswith(" "):
lines = lines[1:] lines = lines[1:]
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
assert lines[1].startswith("Python modules loaded from ") assert lines[1].startswith("Python executable is")
assert lines[2].startswith("Binary modules loaded from ") lines = lines[2:]
assert lines[3] == "-" * 68 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 = ( jpeg = (
"\n" "\n"
+ "-" * 68 + "-" * 68
@ -142,4 +151,4 @@ def test_pilinfo() -> None:
+ "-" * 68 + "-" * 68
+ "\n" + "\n"
) )
assert jpeg in out assert supported_formats == (jpeg in out)

View File

@ -5,7 +5,7 @@ from pathlib import Path
import pytest import pytest
from PIL import BmpImagePlugin, Image from PIL import BmpImagePlugin, Image, _binary
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -128,6 +128,29 @@ def test_load_dib() -> None:
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") 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: def test_save_dib(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.dib") outfile = str(tmp_path / "temp.dib")

View File

@ -336,9 +336,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
strings = ["something", "else", "baz", "bif"] strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = "Failure with line ending: %s" % ( ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
"".join("%s" % ord(s) for s in ending)
)
assert t.readline().strip("\r\n") == "something", ending assert t.readline().strip("\r\n") == "something", ending
assert t.readline().strip("\r\n") == "else", ending assert t.readline().strip("\r\n") == "else", ending
assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "baz", ending

View File

@ -6,7 +6,7 @@ import pytest
from PIL import FitsImagePlugin, Image 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" TEST_FILE = "Tests/images/hopper.fits"
@ -22,6 +22,11 @@ def test_open() -> None:
assert_image_equal(im, hopper("L")) 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: def test_invalid_file() -> None:
# Arrange # Arrange
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"

View File

@ -825,7 +825,7 @@ class TestFileJpeg:
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
# Act / Assert # Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated." # "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) assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self) -> None: def test_invalid_exif(self) -> None:

View File

@ -289,6 +289,16 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA" 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")) @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None: def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im: 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) 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: def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im: with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" 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) hdr = out.read(2)
length = _binary.i16be(hdr) length = _binary.i16be(hdr)
out.seek(length - 2, os.SEEK_CUR) 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)

View File

@ -6,13 +6,13 @@ import itertools
import os import os
import re import re
import sys import sys
from collections import namedtuple
from pathlib import Path from pathlib import Path
from typing import Any, NamedTuple
import pytest import pytest
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features 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 ( from .helper import (
assert_image_equal, assert_image_equal,
@ -185,7 +185,9 @@ class TestFileLibTiff(LibTiffTestCase):
assert field in reloaded, f"{field} not in metadata" assert field in reloaded, f"{field} not in metadata"
@pytest.mark.valgrind_known_error(reason="Known invalid 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 # 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 # any sense, so we're running up against limits where we're asking
# libtiff to do stupid things. # libtiff to do stupid things.
@ -236,94 +238,109 @@ class TestFileLibTiff(LibTiffTestCase):
del new_ifd[338] del new_ifd[338]
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, tiffinfo=new_ifd) 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 = { custom = {
37000 + k: v 37000 + k: v
for k, v in enumerate( for k, v in enumerate(
[ [
tc(4, TiffTags.SHORT, True), Tc(4, TiffTags.SHORT, True),
tc(123456789, TiffTags.LONG, True), Tc(123456789, TiffTags.LONG, True),
tc(-4, TiffTags.SIGNED_BYTE, False), Tc(-4, TiffTags.SIGNED_BYTE, False),
tc(-4, TiffTags.SIGNED_SHORT, False), Tc(-4, TiffTags.SIGNED_SHORT, False),
tc(-123456789, TiffTags.SIGNED_LONG, False), Tc(-123456789, TiffTags.SIGNED_LONG, False),
tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True), Tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
tc(4.25, TiffTags.FLOAT, True), Tc(4.25, TiffTags.FLOAT, True),
tc(4.25, TiffTags.DOUBLE, True), Tc(4.25, TiffTags.DOUBLE, True),
tc("custom tag value", TiffTags.ASCII, True), Tc("custom tag value", TiffTags.ASCII, True),
tc(b"custom tag value", TiffTags.BYTE, True), Tc(b"custom tag value", TiffTags.BYTE, True),
tc((4, 5, 6), TiffTags.SHORT, True), Tc((4, 5, 6), TiffTags.SHORT, True),
tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True), Tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False), Tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False), Tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
tc( Tc(
(-123456789, 9, 34, 234, 219387, -92432323), (-123456789, 9, 34, 234, 219387, -92432323),
TiffTags.SIGNED_LONG, TiffTags.SIGNED_LONG,
False, False,
), ),
tc((4.25, 5.25), TiffTags.FLOAT, True), Tc((4.25, 5.25), TiffTags.FLOAT, True),
tc((4.25, 5.25), TiffTags.DOUBLE, True), Tc((4.25, 5.25), TiffTags.DOUBLE, True),
# array of TIFF_BYTE requires bytes instead of tuple for backwards # array of TIFF_BYTE requires bytes instead of tuple for backwards
# compatibility # compatibility
tc(bytes([4]), TiffTags.BYTE, True), Tc(bytes([4]), TiffTags.BYTE, True),
tc(bytes((4, 9, 10)), TiffTags.BYTE, True), Tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
] ]
) )
} }
libtiffs = [False] def check_tags(
if Image.core.libtiff_support_custom_tags: tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
libtiffs.append(True) ) -> None:
im = hopper()
for libtiff in libtiffs: out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = libtiff im.save(out, tiffinfo=tiffinfo)
def check_tags( with Image.open(out) as reloaded:
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] for tag, value in tiffinfo.items():
) -> None: reloaded_value = reloaded.tag_v2[tag]
im = hopper() 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") assert reloaded_value == value
im.save(out, tiffinfo=tiffinfo)
with Image.open(out) as reloaded: # Test with types
for tag, value in tiffinfo.items(): ifd = TiffImagePlugin.ImageFileDirectory_v2()
reloaded_value = reloaded.tag_v2[tag] for tag, tagdata in custom.items():
if ( ifd[tag] = tagdata.value
isinstance(reloaded_value, TiffImagePlugin.IFDRational) ifd.tagtype[tag] = tagdata.type
and libtiff check_tags(ifd)
):
# libtiff does not support real RATIONALS
assert (
round(abs(float(reloaded_value) - float(value)), 7) == 0
)
continue
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 def test_osubfiletype(self, tmp_path: Path) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() outfile = str(tmp_path / "temp.tif")
for tag, tagdata in custom.items(): with Image.open("Tests/images/g4_orientation_6.tif") as im:
ifd[tag] = tagdata.value im.tag_v2[OSUBFILETYPE] = 1
ifd.tagtype[tag] = tagdata.type im.save(outfile)
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_subifd(self, tmp_path: Path) -> None: def test_subifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
@ -333,24 +350,24 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault # Should not segfault
im.save(outfile) im.save(outfile)
def test_xmlpacket_tag(self, tmp_path: Path) -> None: def test_xmlpacket_tag(
TiffImagePlugin.WRITE_LIBTIFF = True self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
if 700 in reloaded.tag_v2: if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag" 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 # issue #1765
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, dpi=(72, 72)) im.save(out, dpi=(72, 72))
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.info["dpi"] == (72.0, 72.0) 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_v2[269]
assert "temp.tif" == reread.tag[269][0] 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 """Are we generating the same interpretation
of the image as Imagemagick is?""" 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: with Image.open("Tests/images/12bit.cropped.tif") as im:
im.load() im.load()
TiffImagePlugin.READ_LIBTIFF = False monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False)
# to make the target -- # to make the target --
# convert 12bit.cropped.tif -depth 16 tmp.tif # convert 12bit.cropped.tif -depth 16 tmp.tif
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
@ -504,12 +521,13 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) @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") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out) im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
# colormap/palette tag # colormap/palette tag
@ -538,9 +556,9 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(OSError): with pytest.raises(OSError):
os.close(fn) os.close(fn)
def test_multipage(self) -> None: def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None:
# issue #862 # issue #862
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue # 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.size == (20, 20)
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
TiffImagePlugin.READ_LIBTIFF = False def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None:
def test_multipage_nframes(self) -> None:
# issue #862 # issue #862
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
frames = im.n_frames frames = im.n_frames
assert frames == 3 assert frames == 3
@ -572,10 +588,8 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise ValueError: I/O operation on closed file # Should not raise ValueError: I/O operation on closed file
im.load() im.load()
TiffImagePlugin.READ_LIBTIFF = False def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
def test_multipage_seek_backwards(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
im.seek(1) im.seek(1)
im.load() im.load()
@ -583,24 +597,21 @@ class TestFileLibTiff(LibTiffTestCase):
im.seek(0) im.seek(0)
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
TiffImagePlugin.READ_LIBTIFF = False def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
def test__next(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
assert not im.tag.next assert not im.tag.next
im.load() im.load()
assert not im.tag.next assert not im.tag.next
def test_4bit(self) -> None: def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
test_file = "Tests/images/hopper_gray_4bpp.tif" test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L") original = hopper("L")
# Act # Act
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open(test_file) as im: with Image.open(test_file) as im:
TiffImagePlugin.READ_LIBTIFF = False
# Assert # Assert
assert im.size == (128, 128) assert im.size == (128, 128)
@ -640,12 +651,12 @@ class TestFileLibTiff(LibTiffTestCase):
assert im2.mode == "L" assert im2.mode == "L"
assert_image_equal(im, im2) assert_image_equal(im, im2)
def test_save_bytesio(self) -> None: def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None:
# PR 1011 # PR 1011
# Test TIFF saving to io.BytesIO() object. # Test TIFF saving to io.BytesIO() object.
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
# Generate test image # Generate test image
pilim = hopper() pilim = hopper()
@ -662,9 +673,6 @@ class TestFileLibTiff(LibTiffTestCase):
save_bytesio("packbits") save_bytesio("packbits")
save_bytesio("tiff_lzw") save_bytesio("tiff_lzw")
TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_LIBTIFF = False
def test_save_ycbcr(self, tmp_path: Path) -> None: def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr") im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
@ -684,15 +692,16 @@ class TestFileLibTiff(LibTiffTestCase):
if Image.core.libtiff_support_custom_tags: if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456 assert reloaded.tag_v2[34665] == 125456
def test_crashing_metadata(self, tmp_path: Path) -> None: def test_crashing_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# issue 1597 # issue 1597
with Image.open("Tests/images/rdf.tif") as im: with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
# this shouldn't crash # this shouldn't crash
im.save(out, format="TIFF") im.save(out, format="TIFF")
TiffImagePlugin.WRITE_LIBTIFF = False
def test_page_number_x_0(self, tmp_path: Path) -> None: def test_page_number_x_0(self, tmp_path: Path) -> None:
# Issue 973 # Issue 973
@ -723,36 +732,41 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise PermissionError. # Should not raise PermissionError.
os.remove(tmpfile) 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: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc = img.info.get("icc_profile") icc = img.info.get("icc_profile")
assert icc is not None 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: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_libtiff = img.info.get("icc_profile") icc_libtiff = img.info.get("icc_profile")
assert icc_libtiff is not None assert icc_libtiff is not None
TiffImagePlugin.READ_LIBTIFF = False
assert icc == icc_libtiff assert icc == icc_libtiff
def test_write_icc(self, tmp_path: Path) -> None: @pytest.mark.parametrize(
def check_write(libtiff: bool) -> None: "libtiff",
TiffImagePlugin.WRITE_LIBTIFF = 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: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"] icc_profile = img.info["icc_profile"]
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
img.save(out, icc_profile=icc_profile) img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"] 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)
def test_multipage_compression(self) -> None: def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im: 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") 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)) im = Image.new("F", (1, 1))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out) im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.mode == "F" assert reloaded.mode == "F"
@ -1081,15 +1096,14 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im: with Image.open(out) as im:
im.load() im.load()
def test_realloc_overflow(self) -> None: def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e: with pytest.raises(OSError) as e:
im.load() im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY # Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9" assert str(e.value) == "-9"
TiffImagePlugin.READ_LIBTIFF = False
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:

39
Tests/test_file_mpeg.py Normal file
View 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()

View File

@ -93,7 +93,7 @@ def test_exif(test_file: str) -> None:
def test_frame_size() -> None: def test_frame_size() -> None:
# This image has been hexedited to contain a different size # 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: with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
assert im.size == (640, 480) assert im.size == (640, 480)

View File

@ -85,7 +85,9 @@ class TestFilePng:
def test_sanity(self, tmp_path: Path) -> None: def test_sanity(self, tmp_path: Path) -> None:
# internal version number # 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") test_file = str(tmp_path / "temp.png")

View File

@ -241,13 +241,23 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
im.load() 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: def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256") f.write(b"P3\n128 128\n255\n256")
with Image.open(path) as im: with Image.open(path) as im:
with pytest.raises(ValueError): with pytest.raises(ValueError, match="Channel value too large"):
im.load() im.load()

View File

@ -113,6 +113,10 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) 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: def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:

View File

@ -151,3 +151,15 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
target = im.convert("RGBA") target = im.convert("RGBA")
assert_image_similar(image, target, 25.0) 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()

View File

@ -188,3 +188,21 @@ def test_seek_errors() -> None:
with pytest.raises(EOFError): with pytest.raises(EOFError):
im.seek(42) 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()

View File

@ -28,43 +28,26 @@ from .helper import (
assert_image_similar_tofile, assert_image_similar_tofile,
assert_not_all_same, assert_not_all_same,
hopper, hopper,
is_big_endian,
is_win32, is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, 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: class TestImage:
@pytest.mark.parametrize( @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
"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",
),
)
def test_image_modes_success(self, mode: str) -> None: 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")) @pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None: def test_image_modes_fail(self, mode: str) -> None:
@ -1042,6 +1025,38 @@ class TestImage:
assert im.fp is None 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): class MockEncoder(ImageFile.PyEncoder):
pass pass

View File

@ -33,7 +33,7 @@ except ImportError:
class AccessTest: class AccessTest:
# initial value # Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS _init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False _need_cffi_access = False
@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest):
if bands == 1: if bands == 1:
return 1 return 1
if mode in ("BGR;15", "BGR;16"): if mode in ("BGR;15", "BGR;16"):
# These modes have less than 8 bits per band # These modes have less than 8 bits per band,
# So (1, 2, 3) cannot be roundtripped # so (1, 2, 3) cannot be roundtripped.
return (16, 32, 49) return (16, 32, 49)
return tuple(range(1, bands + 1)) 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 self.color(mode) if expected_color_int is None else expected_color_int
) )
# check putpixel # Check putpixel
im = Image.new(mode, (1, 1), None) im = Image.new(mode, (1, 1), None)
im.putpixel((0, 0), expected_color) im.putpixel((0, 0), expected_color)
actual_color = im.getpixel((0, 0)) actual_color = im.getpixel((0, 0))
@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# check putpixel negative index # Check putpixel negative index
im.putpixel((-1, -1), expected_color) im.putpixel((-1, -1), expected_color)
actual_color = im.getpixel((-1, -1)) actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, ( assert actual_color == expected_color, (
@ -168,22 +168,21 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# Check 0 # Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None) im = Image.new(mode, (0, 0), None)
assert im.load() is not None assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error): with pytest.raises(error):
im.putpixel((0, 0), expected_color) im.putpixel((0, 0), expected_color)
with pytest.raises(error): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check negative index
with pytest.raises(error): with pytest.raises(error):
im.putpixel((-1, -1), expected_color) im.putpixel((-1, -1), expected_color)
with pytest.raises(error): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
# check initial color # Check initial color
im = Image.new(mode, (1, 1), expected_color) im = Image.new(mode, (1, 1), expected_color)
actual_color = im.getpixel((0, 0)) actual_color = im.getpixel((0, 0))
assert actual_color == expected_color, ( assert actual_color == expected_color, (
@ -191,46 +190,30 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# check initial color negative index # Check initial color negative index
actual_color = im.getpixel((-1, -1)) actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, ( assert actual_color == expected_color, (
f"initial color failed with negative index for mode {mode}, " f"initial color failed with negative index for mode {mode}, "
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# Check 0 # Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color) im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check negative index
with pytest.raises(error): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
@pytest.mark.parametrize( @pytest.mark.parametrize("mode", Image.MODES)
"mode",
(
"1",
"L",
"LA",
"I",
"I;16",
"I;16B",
"F",
"P",
"PA",
"BGR;15",
"BGR;16",
"BGR;24",
"RGB",
"RGBA",
"RGBX",
"CMYK",
"YCbCr",
),
)
def test_basic(self, mode: str) -> None: def test_basic(self, mode: str) -> None:
self.check(mode) 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: def test_list(self) -> None:
im = hopper() im = hopper()
assert im.getpixel([0, 0]) == (20, 20, 70) 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("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
def test_signedness(self, mode: str, expected_color: int) -> None: def test_signedness(self, mode: str, expected_color: int) -> None:
# see https://github.com/python-pillow/Pillow/issues/452 # See https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint* # pixelaccess is using signed int* instead of uint*
self.check(mode, expected_color) self.check(mode, expected_color)
@ -298,13 +281,6 @@ class TestCffi(AccessTest):
im = Image.new(mode, (10, 10), 40000) im = Image.new(mode, (10, 10), 40000)
self._test_get_access(im) 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: def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image? """Are we writing the correct bits into the image?
@ -336,23 +312,18 @@ class TestCffi(AccessTest):
self._test_set_access(hopper("LA"), (128, 128)) self._test_set_access(hopper("LA"), (128, 128))
self._test_set_access(hopper("1"), 255) self._test_set_access(hopper("1"), 255)
self._test_set_access(hopper("P"), 128) 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) self._test_set_access(hopper("F"), 1024.0)
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000) im = Image.new(mode, (10, 10), 40000)
self._test_set_access(im, 45000) 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") @pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_not_implemented(self) -> None: def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is 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: def test_reference_counting(self) -> None:
size = 10 size = 10
@ -361,7 +332,7 @@ class TestCffi(AccessTest):
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load() px = Image.new("L", (size, 1), 0).load()
for i in range(size): 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 assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
@ -439,13 +410,14 @@ class TestEmbeddable:
from setuptools.command import build_ext from setuptools.command import build_ext
with open("embed_pil.c", "w", encoding="utf-8") as fh: with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\")
fh.write( fh.write(
""" f"""
#include "Python.h" #include "Python.h"
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {{
char *home = "%s"; char *home = "{home}";
wchar_t *whome = Py_DecodeLocale(home, NULL); wchar_t *whome = Py_DecodeLocale(home, NULL);
Py_SetPythonHome(whome); Py_SetPythonHome(whome);
@ -460,9 +432,8 @@ int main(int argc, char* argv[])
PyMem_RawFree(whome); PyMem_RawFree(whome);
return 0; return 0;
} }}
""" """
% sys.prefix.replace("\\", "\\\\")
) )
compiler = getattr(build_ext, "new_compiler")() compiler = getattr(build_ext, "new_compiler")()
@ -478,7 +449,7 @@ int main(int argc, char* argv[])
env = os.environ.copy() env = os.environ.copy()
env["PATH"] = sys.prefix + ";" + env["PATH"] env["PATH"] = sys.prefix + ";" + env["PATH"]
# do not display the Windows Error Reporting dialog # Do not display the Windows Error Reporting dialog
getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
process = subprocess.Popen(["embed_pil.exe"], env=env) process = subprocess.Popen(["embed_pil.exe"], env=env)

View File

@ -91,6 +91,16 @@ def test_fromarray() -> None:
Image.fromarray(wrapped) 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: def test_fromarray_palette() -> None:
# Arrange # Arrange
i = im.convert("L") i = im.convert("L")

View File

@ -183,6 +183,14 @@ def test_trns_RGB(tmp_path: Path) -> None:
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
im_l.save(f) 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") im_p = im.convert("P")
assert "transparency" in im_p.info assert "transparency" in im_p.info
im_p.save(f) im_p.save(f)
@ -191,6 +199,10 @@ def test_trns_RGB(tmp_path: Path) -> None:
assert "transparency" not in im_rgba.info assert "transparency" not in im_rgba.info
im_rgba.save(f) 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) im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE)
assert "transparency" not in im_p.info assert "transparency" not in im_p.info
im_p.save(f) im_p.save(f)

View File

@ -16,11 +16,13 @@ pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed" not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
) )
ims = [ ims: list[Image.Image] = []
hopper(),
Image.open("Tests/images/transparent.png"),
Image.open("Tests/images/7x13.png"), 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: def teardown_module() -> None:

View File

@ -14,7 +14,7 @@ def test_sanity() -> None:
assert data[0] == (20, 20, 70) assert data[0] == (20, 20, 70)
def test_roundtrip() -> None: def test_mode() -> None:
def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]:
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
data = im.getdata() data = im.getdata()

View File

@ -81,7 +81,8 @@ def test_mode_F() -> None:
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_mode_BGR(mode: str) -> None: def test_mode_BGR(mode: str) -> None:
data = [(16, 32, 49), (32, 32, 98)] data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2)) with pytest.warns(DeprecationWarning):
im = Image.new(mode, (1, 2))
im.putdata(data) im.putdata(data)
assert list(im.getdata()) == data assert list(im.getdata()) == data

View File

@ -94,6 +94,19 @@ def test_quantize_dither_diff() -> None:
assert dither.tobytes() != nodither.tobytes() 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: def test_colors() -> None:
im = hopper() im = hopper()
colors = 2 colors = 2

View File

@ -186,7 +186,9 @@ def assert_compare_images(
bands = ImageMode.getmode(a.mode).bands bands = ImageMode.getmode(a.mode).bands
for band, ach, bch in zip(bands, a.split(), b.split()): 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() ch_hist = ch_diff.histogram()
average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( average_diff = sum(i * num for i, num in enumerate(ch_hist)) / (

View File

@ -284,7 +284,7 @@ class TestCoreResampleAlphaCorrect:
used_colors = {px[x, y][0] for x in range(i.size[0])} used_colors = {px[x, y][0] for x in range(i.size[0])}
assert 256 == len(used_colors), ( assert 256 == len(used_colors), (
"All colors should be present in resized image. " "All colors should be present in resized image. "
f"Only {len(used_colors)} on {y} line." f"Only {len(used_colors)} on line {y}."
) )
@pytest.mark.xfail(reason="Current implementation isn't precise enough") @pytest.mark.xfail(reason="Current implementation isn't precise enough")

View File

@ -4,13 +4,14 @@ import datetime
import os import os
import re import re
import shutil import shutil
import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
import pytest import pytest
from PIL import Image, ImageMode, features from PIL import Image, ImageMode, ImageWin, features
from .helper import ( from .helper import (
assert_image, assert_image,
@ -18,6 +19,7 @@ from .helper import (
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
is_pypy,
) )
try: try:
@ -213,6 +215,10 @@ def test_display_profile() -> None:
# try fetching the profile for the current display device # try fetching the profile for the current display device
ImageCms.get_display_profile() 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: def test_lab_color_profile() -> None:
ImageCms.createProfile("LAB", 5000) ImageCms.createProfile("LAB", 5000)
@ -496,16 +502,34 @@ def test_non_ascii_path(tmp_path: Path) -> None:
def test_profile_typesafety() -> None: def test_profile_typesafety() -> None:
"""Profile init type safety # does not segfault
prepatch, these would segfault, postpatch they should emit a typeerror
"""
with pytest.raises(TypeError, match="Invalid type for Profile"): with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(0).tobytes() ImageCms.ImageCmsProfile(0).tobytes()
with pytest.raises(TypeError, match="Invalid type for Profile"): with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(1).tobytes() 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( def assert_aux_channel_preserved(
mode: str, transform_in_place: bool, preserved_channel: str 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) 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")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
def test_rgb_lab(mode: str) -> None: def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1)) im = Image.new(mode, (1, 1))

View File

@ -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"

View 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"
)

View 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"

View File

@ -47,7 +47,11 @@ def test_iterator_min_frame() -> None:
assert i[index] == next(i) 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: with Image.open("Tests/images/multipage.tiff") as im:
for index, frame in enumerate(ImageSequence.Iterator(im)): for index, frame in enumerate(ImageSequence.Iterator(im)):
frame.load() frame.load()
@ -55,17 +59,6 @@ def _test_multipage_tiff() -> None:
frame.convert("RGB") 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: def test_consecutive() -> None:
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
first_frame = None first_frame = None

View File

@ -15,7 +15,7 @@ class TestLibPack:
mode: str, mode: str,
rawmode: str, rawmode: str,
data: int | bytes, data: int | bytes,
*pixels: int | float | tuple[int, ...], *pixels: float | tuple[int, ...],
) -> None: ) -> None:
""" """
data - either raw bytes with data or just number of bytes in rawmode. data - either raw bytes with data or just number of bytes in rawmode.
@ -216,7 +216,10 @@ class TestLibPack:
) )
def test_I16(self) -> None: 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: def test_F_float(self) -> None:
self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34)
@ -239,7 +242,7 @@ class TestLibUnpack:
mode: str, mode: str,
rawmode: str, rawmode: str,
data: int | bytes, data: int | bytes,
*pixels: int | float | tuple[int, ...], *pixels: float | tuple[int, ...],
) -> None: ) -> None:
""" """
data - either raw bytes with data or just number of bytes in rawmode. data - either raw bytes with data or just number of bytes in rawmode.
@ -359,11 +362,14 @@ class TestLibUnpack:
) )
def test_BGR(self) -> None: def test_BGR(self) -> None:
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) with pytest.warns(DeprecationWarning):
self.assert_unpack( self.assert_unpack(
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)
) )
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) 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: def test_RGBA(self) -> None:
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))

View File

@ -19,7 +19,7 @@ from PIL import Image
# 7 # 7
# 160 # 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" path = "Tests/images/hopper.jpg"

View File

@ -4,9 +4,16 @@ import os
import subprocess import subprocess
import sys 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() lines = out.splitlines()
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
assert lines[1].startswith("Pillow ") assert lines[1].startswith("Pillow ")
@ -15,9 +22,15 @@ def test_main() -> None:
while lines[0].startswith(" "): while lines[0].startswith(" "):
lines = lines[1:] lines = lines[1:]
assert lines[0] == "-" * 68 assert lines[0] == "-" * 68
assert lines[1].startswith("Python modules loaded from ") assert lines[1].startswith("Python executable is")
assert lines[2].startswith("Binary modules loaded from ") lines = lines[2:]
assert lines[3] == "-" * 68 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 = ( jpeg = (
os.linesep os.linesep
+ "-" * 68 + "-" * 68
@ -31,4 +44,4 @@ def test_main() -> None:
+ "-" * 68 + "-" * 68
+ os.linesep + os.linesep
) )
assert jpeg in out assert report == (jpeg not in out)

View File

@ -3,10 +3,12 @@ from __future__ import annotations
from fractions import Fraction from fractions import Fraction
from pathlib import Path from pathlib import Path
from PIL import Image, TiffImagePlugin, features import pytest
from PIL import Image, TiffImagePlugin
from PIL.TiffImagePlugin import IFDRational from PIL.TiffImagePlugin import IFDRational
from .helper import hopper from .helper import hopper, skip_unless_feature
def _test_equal(num, denom, target) -> None: def _test_equal(num, denom, target) -> None:
@ -52,18 +54,18 @@ def test_nonetype() -> None:
assert xres and yres assert xres and yres
def test_ifd_rational_save(tmp_path: Path) -> None: @pytest.mark.parametrize(
methods = [True] "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
if features.check("libtiff"): )
methods.append(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: monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
TiffImagePlugin.WRITE_LIBTIFF = libtiff im.save(out, dpi=(res, res), compression="raw")
im = hopper() with Image.open(out) as reloaded:
out = str(tmp_path / "temp.tiff") assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
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])

View File

@ -11,41 +11,12 @@ backend_class = build_wheel.__self__.__class__
class _CustomBuildMetaBackend(backend_class): class _CustomBuildMetaBackend(backend_class):
def run_setup(self, setup_script="setup.py"): def run_setup(self, setup_script="setup.py"):
if self.config_settings: 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) return super().run_setup(setup_script)
def build_wheel( def build_wheel(
@ -54,5 +25,15 @@ class _CustomBuildMetaBackend(backend_class):
self.config_settings = config_settings self.config_settings = config_settings
return super().build_wheel(wheel_directory, config_settings, metadata_directory) 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

View File

@ -1,11 +1,11 @@
The Python Imaging Library (PIL) is The Python Imaging Library (PIL) is
Copyright © 1997-2011 by Secret Labs AB 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 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 Like PIL, Pillow is licensed under the open source PIL
Software License: Software License:

View File

@ -46,7 +46,7 @@ clean:
-rm -rf $(BUILDDIR)/* -rm -rf $(BUILDDIR)/*
install-sphinx: 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 .PHONY: html
html: html:

View File

@ -22,19 +22,19 @@ import PIL
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # 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 # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
"dater",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.extlinks", "sphinx.ext.extlinks",
"sphinx.ext.intersphinx", "sphinx.ext.intersphinx",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinx_copybutton", "sphinx_copybutton",
"sphinx_inline_tabs", "sphinx_inline_tabs",
"sphinx_removed_in",
"sphinxext.opengraph", "sphinxext.opengraph",
] ]
@ -54,9 +54,10 @@ master_doc = "index"
# General information about the project. # General information about the project.
project = "Pillow (PIL Fork)" project = "Pillow (PIL Fork)"
copyright = ( 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 # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -252,7 +253,7 @@ latex_documents = [
master_doc, master_doc,
"PillowPILFork.tex", "PillowPILFork.tex",
"Pillow (PIL Fork) Documentation", "Pillow (PIL Fork) Documentation",
"Jeffrey A. Clark (Alex)", "Jeffrey A. Clark",
"manual", "manual",
) )
] ]
@ -302,7 +303,7 @@ texinfo_documents = [
"Pillow (PIL Fork) Documentation", "Pillow (PIL Fork) Documentation",
author, author,
"PillowPILFork", "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", "Miscellaneous",
) )
] ]

48
docs/dater.py Normal file
View 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}

View File

@ -92,6 +92,29 @@ Deprecated Use instead
:py:data:`sys.version_info`, and ``PIL.__version__`` :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 Removed features
---------------- ----------------

View File

@ -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;16L`` (16-bit little endian unsigned integer pixels)
* ``I;16B`` (16-bit big endian unsigned integer pixels) * ``I;16B`` (16-bit big endian unsigned integer pixels)
* ``I;16N`` (16-bit native 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 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)`` multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)``

View File

@ -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. If present and true, instructs the WebP writer to use lossless compression.
**quality** **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 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 of effort put into the compression: 0 is the fastest, but gives larger
files compared to the slowest, but best, 100. 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** **method**
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4.
@ -1335,7 +1339,8 @@ FITS
.. versionadded:: 9.1.0 .. 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 FLI, FLC
^^^^^^^^ ^^^^^^^^
@ -1351,9 +1356,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
FPX FPX
^^^ ^^^
Pillow reads Kodak FlashPix files. In the current version, only the highest Pillow reads Kodak FlashPix files. Only the highest resolution image is read from the
resolution image is read from the file, and the viewing transform is not taken file, and the viewing transform is not taken into account.
into account.
To enable FPX support, you must install :pypi:`olefile`. To enable FPX support, you must install :pypi:`olefile`.
@ -1484,7 +1488,9 @@ QOI
.. versionadded:: 9.5.0 .. 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 SUN
^^^ ^^^

View File

@ -1,7 +1,7 @@
Pillow 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>`_. 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>`_.

View File

@ -266,9 +266,10 @@ After navigating to the Pillow directory, run::
Build Options Build Options
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use * Config setting: ``-C parallel=n``. Can also be given
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
sets the number of CPUs to use, or can disable parallel building by 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 using a setting of 1. By default, it uses 4 CPUs, or if 4 are not
available, as many as are present. available, as many as are present.
@ -293,14 +294,13 @@ Build Options
used to compile the standard Pillow wheels. Compiling libraqm requires used to compile the standard Pillow wheels. Compiling libraqm requires
a C99-compliant compiler. 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 platform dependent guessing of include and library directories for
automated build systems that configure the proper paths in the automated build systems that configure the proper paths in the
environment variables (e.g. Buildroot). environment variables (e.g. Buildroot).
* Build flag: ``-C debug=true``. Adds a debugging flag to the include and * 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 library search process to dump all paths searched for and found to stdout.
stdout.
Sample usage:: Sample usage::

View File

@ -25,23 +25,19 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Arch | 3.9 | x86-64 | | 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 | | CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86-64 | | Debian 11 Bullseye | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 | | Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 | | Fedora 39 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | 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 | | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | PyPy3 | | | | 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 | | Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | | | | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.10 | arm64v8, ppc64le, | | | 3.10 | arm64v8 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x | | | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.8 | x86-64 | | 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 | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | 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 | | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +----------------------------+------------------+ | | +----------------------------+------------------+ |

View File

@ -78,6 +78,8 @@ Constructing images
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
.. autofunction:: new .. autofunction:: new
.. autoclass:: SupportsArrayInterface
:show-inheritance:
.. autofunction:: fromarray .. autofunction:: fromarray
.. autofunction:: frombytes .. autofunction:: frombytes
.. autofunction:: frombuffer .. autofunction:: frombuffer

View File

@ -73,7 +73,7 @@ can be easily displayed in a chromaticity diagram, for example).
:canonical: PIL._imagingcms.CmsProfile :canonical: PIL._imagingcms.CmsProfile
.. py:attribute:: creation_date .. 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). 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). not been calculated (see 7.2.18 of ICC.1:2010).
.. py:attribute:: copyright .. py:attribute:: copyright
:type: Optional[str] :type: str | None
The text copyright information for the profile (see 9.2.21 of ICC.1:2010). The text copyright information for the profile (see 9.2.21 of ICC.1:2010).
.. py:attribute:: manufacturer .. py:attribute:: manufacturer
:type: Optional[str] :type: str | None
The (English) display string for the device manufacturer (see The (English) display string for the device manufacturer (see
9.2.22 of ICC.1:2010). 9.2.22 of ICC.1:2010).
.. py:attribute:: model .. py:attribute:: model
:type: Optional[str] :type: str | None
The (English) display string for the device model of the device 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). for which this profile is created (see 9.2.23 of ICC.1:2010).
.. py:attribute:: profile_description .. py:attribute:: profile_description
:type: Optional[str] :type: str | None
The (English) display string for the profile description (see The (English) display string for the profile description (see
9.2.41 of ICC.1:2010). 9.2.41 of ICC.1:2010).
.. py:attribute:: target .. py:attribute:: target
:type: Optional[str] :type: str | None
The name of the registered characterization data set, or the The name of the registered characterization data set, or the
measurement data for a characterization target (see 9.2.14 of measurement data for a characterization target (see 9.2.14 of
ICC.1:2010). ICC.1:2010).
.. py:attribute:: red_colorant .. 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 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. The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: green_colorant .. 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 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. The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: blue_colorant .. 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 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. The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: luminance .. 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 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). 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. The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: chromaticity .. 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, The data of the phosphor/colorant chromaticity set used (red,
green and blue channels, see 9.2.16 of ICC.1:2010). 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. The value is in the format ``((x, y, Y), (x, y, Y), (x, y, Y))``, if available.
.. py:attribute:: chromatic_adaption .. 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 The chromatic adaption matrix converts a color measured using the
actual illumination conditions and relative to the actual adopted 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). 9.2.19 of ICC.1:2010).
.. py:attribute:: colorimetric_intent .. py:attribute:: colorimetric_intent
:type: Optional[str] :type: str | None
4-character string (padded with whitespace) identifying the image 4-character string (padded with whitespace) identifying the image
state of PCS colorimetry produced using the colorimetric intent state of PCS colorimetry produced using the colorimetric intent
transforms (see 9.2.20 of ICC.1:2010 for details). transforms (see 9.2.20 of ICC.1:2010 for details).
.. py:attribute:: perceptual_rendering_intent_gamut .. py:attribute:: perceptual_rendering_intent_gamut
:type: Optional[str] :type: str | None
4-character string (padded with whitespace) identifying the (one) 4-character string (padded with whitespace) identifying the (one)
standard reference medium gamut (see 9.2.37 of ICC.1:2010 for standard reference medium gamut (see 9.2.37 of ICC.1:2010 for
details). details).
.. py:attribute:: saturation_rendering_intent_gamut .. py:attribute:: saturation_rendering_intent_gamut
:type: Optional[str] :type: str | None
4-character string (padded with whitespace) identifying the (one) 4-character string (padded with whitespace) identifying the (one)
standard reference medium gamut (see 9.2.37 of ICC.1:2010 for standard reference medium gamut (see 9.2.37 of ICC.1:2010 for
details). details).
.. py:attribute:: technology .. py:attribute:: technology
:type: Optional[str] :type: str | None
4-character string (padded with whitespace) identifying the device 4-character string (padded with whitespace) identifying the device
technology (see 9.2.47 of ICC.1:2010 for details). technology (see 9.2.47 of ICC.1:2010 for details).
.. py:attribute:: media_black_point .. 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 This tag specifies the media black point and is used for
generating absolute colorimetry. 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. The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: media_white_point_temperature .. py:attribute:: media_white_point_temperature
:type: Optional[float] :type: float | None
Calculates the white point temperature (see the LCMS documentation Calculates the white point temperature (see the LCMS documentation
for more information). for more information).
.. py:attribute:: viewing_condition .. py:attribute:: viewing_condition
:type: Optional[str] :type: str | None
The (English) display string for the viewing conditions (see The (English) display string for the viewing conditions (see
9.2.48 of ICC.1:2010). 9.2.48 of ICC.1:2010).
.. py:attribute:: screening_description .. py:attribute:: screening_description
:type: Optional[str] :type: str | None
The (English) display string for the screening conditions. The (English) display string for the screening conditions.
@ -307,21 +307,21 @@ can be easily displayed in a chromaticity diagram, for example).
version 4. version 4.
.. py:attribute:: red_primary .. 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 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. The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: green_primary .. 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 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. The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: blue_primary .. 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). 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). documentation on LCMS).
.. py:attribute:: clut .. 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 Returns a dictionary of all supported intents and directions for
the CLUT model. the CLUT model.
@ -353,7 +353,7 @@ can be easily displayed in a chromaticity diagram, for example).
that intent is supported for that direction. that intent is supported for that direction.
.. py:attribute:: intent_supported .. 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. 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: 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. Returns if the intent is supported for the given direction.

View File

@ -23,8 +23,7 @@ Example: Filter an image
Filters Filters
------- -------
The current version of the library provides the following set of predefined Pillow provides the following set of predefined image enhancement filters:
image enhancement filters:
* **BLUR** * **BLUR**
* **CONTOUR** * **CONTOUR**

View File

@ -4,9 +4,12 @@
:py:mod:`~PIL.ImageMath` Module :py:mod:`~PIL.ImageMath` Module
=============================== ===============================
The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”. The The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that
module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes can take a number of images and generate a result.
an expression string and one or more images.
: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 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("image1.jpg") as im1:
with Image.open("image2.jpg") as im2: 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) .. py:function:: lambda_eval(expression, options)
out.save("result.png")
.. 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 .. py:function:: unsafe_eval(expression, options)
single-layer images. To process multi-band images, use the
:py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` Evaluates an image expression.
function.
.. 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 :param expression: A string which uses the standard Python expression
syntax. In addition to the standard operators, you can syntax. In addition to the standard operators, you can
also use the functions described below. also use the functions described below.
:param environment: A dictionary that maps image names to Image instances. :param options: Values to add to the function's dictionary, mapping image
You can use one or more keyword arguments instead of a names to Image instances. You can use one or more keyword
dictionary, as shown in the above example. Note that arguments instead of a dictionary, as shown in the above
the names must be valid Python identifiers. example. Note that the names must be valid Python
identifiers.
:return: An image, an integer value, a floating point value, :return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression. or a pixel tuple, depending on the expression.
Expression syntax Expression syntax
----------------- -----------------
Expressions are standard Python expressions, but theyre evaluated in a * :py:meth:`lambda_eval` expressions are functions that receive a dictionary
non-standard environment. You can use PIL methods as usual, plus the following containing images and operators.
set of operators and functions:
* :py:meth:`unsafe_eval` expressions are standard Python expressions,
but theyre 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 Standard Operators
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^

View File

@ -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 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 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 slicing as usual. However, this does not support slice assignment, or item
assignment, or item and slice deletion. and slice deletion.
:param xy: A sequence. The sequence can contain 2-tuples [(x, y), ...] :param xy: A sequence. The sequence can contain 2-tuples [(x, y), ...]
or a flat list of numbers [x, y, ...]. or a flat list of numbers [x, y, ...].

View File

@ -7,67 +7,6 @@
The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or
for a region of an image. for a region of an image.
.. py:class:: Stat(image_or_list, mask=None) .. autoclass:: Stat
:members:
Calculate statistics for the given image. If a mask is included, :special-members: __init__
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.

View File

@ -29,7 +29,7 @@ they do not extend beyond the bitmap image.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If an attacker has control over the keys passed to the 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 arbitrary code. To prevent this, keys matching the names of builtins and keys
containing double underscores will now raise a :py:exc:`ValueError`. containing double underscores will now raise a :py:exc:`ValueError`.

View File

@ -4,21 +4,21 @@
Security 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 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.
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations Deprecations
============ ============
@ -58,13 +58,34 @@ Deprecated Use instead
:py:data:`sys.version_info`, and ``PIL.__version__`` :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 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 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 Python's Global Interpreter Lock is now released when fetching WebP frames from
the libwebp decoder. 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.

View 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

View File

@ -47,7 +47,7 @@ Google's `OSS-Fuzz`_ project for finding this issue.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To limit :py:class:`PIL.ImageMath` to working with images, Pillow 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 help prevent problems arising if users evaluate arbitrary expressions, such as
``ImageMath.eval("exec(exit())")``. ``ImageMath.eval("exec(exit())")``.

View File

@ -18,7 +18,7 @@ has been present since PIL.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
While Pillow 9.0 restricted top-level builtins available to 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. available to lambda expressions. These are now also restricted.
Other Changes Other Changes

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
10.4.0
10.3.0 10.3.0
10.2.0 10.2.0
10.1.0 10.1.0

View File

@ -15,7 +15,7 @@ keywords = [
"Imaging", "Imaging",
] ]
license = {text = "HPND"} 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" requires-python = ">=3.8"
classifiers = [ classifiers = [
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
@ -42,10 +42,9 @@ dynamic = [
docs = [ docs = [
"furo", "furo",
"olefile", "olefile",
"sphinx>=2.4", "sphinx>=7.3",
"sphinx-copybutton", "sphinx-copybutton",
"sphinx-inline-tabs", "sphinx-inline-tabs",
"sphinx-removed-in",
"sphinxext-opengraph", "sphinxext-opengraph",
] ]
fpx = [ 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-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests" test-extras = "tests"
[tool.ruff]
fix = true
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
@ -106,6 +108,7 @@ select = [
"ISC", # flake8-implicit-str-concat "ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging "LOG", # flake8-logging
"PGH", # pygrep-hooks "PGH", # pygrep-hooks
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa) "RUF100", # unused noqa (yesqa)
"UP", # pyupgrade "UP", # pyupgrade
"W", # pycodestyle warnings "W", # pycodestyle warnings
@ -116,6 +119,8 @@ ignore = [
"E221", # Multiple spaces before operator "E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator "E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ',' "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] [tool.ruff.lint.per-file-ignores]

View File

@ -139,7 +139,9 @@ def testimage() -> None:
In 1.1.6, you can use the ImageMath module to do image In 1.1.6, you can use the ImageMath module to do image
calculations. 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 >>> im.mode, im.size
('F', (128, 128)) ('F', (128, 128))
@ -163,9 +165,9 @@ if __name__ == "__main__":
print("Running selftest:") print("Running selftest:")
status = doctest.testmod(sys.modules[__name__]) status = doctest.testmod(sys.modules[__name__])
if status[0]: if status[0]:
print("*** %s tests of %d failed." % status) print(f"*** {status[0]} tests of {status[1]} failed.")
exit_status = 1 exit_status = 1
else: else:
print("--- %s tests passed." % status[1]) print(f"--- {status[1]} tests passed.")
sys.exit(exit_status) sys.exit(exit_status)

View File

@ -23,8 +23,10 @@ from setuptools.command.build_ext import build_ext
def get_version(): def get_version():
version_file = "src/PIL/_version.py" version_file = "src/PIL/_version.py"
with open(version_file, encoding="utf-8") as f: with open(version_file, encoding="utf-8") as f:
exec(compile(f.read(), version_file, "exec")) return f.read().split('"')[1]
return locals()["__version__"]
configuration = {}
PILLOW_VERSION = get_version() PILLOW_VERSION = get_version()
@ -334,15 +336,24 @@ class pil_build_ext(build_ext):
+ [("add-imaging-libs=", None, "Add libs to _imaging build")] + [("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): def initialize_options(self):
self.disable_platform_guessing = None self.disable_platform_guessing = self.check_configuration(
"platform-guessing", "disable"
)
self.add_imaging_libs = "" self.add_imaging_libs = ""
build_ext.initialize_options(self) build_ext.initialize_options(self)
for x in self.feature: for x in self.feature:
setattr(self, f"disable_{x}", None) setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
setattr(self, f"enable_{x}", None) setattr(self, f"enable_{x}", self.check_configuration(x, "enable"))
for x in ("raqm", "fribidi"): 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): def finalize_options(self):
build_ext.finalize_options(self) build_ext.finalize_options(self)
@ -1007,6 +1018,12 @@ ext_modules = [
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), 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: try:
setup( setup(
cmdclass={"build_ext": pil_build_ext}, 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. a required dependency when compiling Pillow from source.
Please see the install instructions at: 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) sys.stderr.write(msg)

View File

@ -241,7 +241,7 @@ class BLPFormatError(NotImplementedError):
pass pass
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] in (b"BLP1", b"BLP2") return prefix[:4] in (b"BLP1", b"BLP2")
@ -253,7 +253,7 @@ class BlpImageFile(ImageFile.ImageFile):
format = "BLP" format = "BLP"
format_description = "Blizzard Mipmap Format" format_description = "Blizzard Mipmap Format"
def _open(self): def _open(self) -> None:
self.magic = self.fp.read(4) self.magic = self.fp.read(4)
self.fp.seek(5, os.SEEK_CUR) self.fp.seek(5, os.SEEK_CUR)
@ -333,7 +333,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder): class BLP1Decoder(_BLPBaseDecoder):
def _load(self): def _load(self) -> None:
if self._blp_compression == Format.JPEG: if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream() self._decode_jpeg_stream()
@ -341,7 +341,7 @@ class BLP1Decoder(_BLPBaseDecoder):
if self._blp_encoding in (4, 5): if self._blp_encoding in (4, 5):
palette = self._read_palette() palette = self._read_palette()
data = self._read_bgra(palette) data = self._read_bgra(palette)
self.set_as_raw(bytes(data)) self.set_as_raw(data)
else: else:
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
@ -412,13 +412,13 @@ class BLP2Decoder(_BLPBaseDecoder):
msg = f"Unknown BLP compression {repr(self._blp_compression)}" msg = f"Unknown BLP compression {repr(self._blp_compression)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
self.set_as_raw(bytes(data)) self.set_as_raw(data)
class BLPEncoder(ImageFile.PyEncoder): class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True _pushes_fd = True
def _write_palette(self): def _write_palette(self) -> bytes:
data = b"" data = b""
palette = self.im.getpalette("RGBA", "RGBA") palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4): for i in range(len(palette) // 4):

View File

@ -48,12 +48,12 @@ BIT2MODE = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM" return prefix[:2] == b"BM"
def _dib_accept(prefix): 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 # read the rest of the bmp header, without its size
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) 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 # ----- This format has different offsets because of width/height types
# 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
if file_info["header_size"] == 12: if file_info["header_size"] == 12:
file_info["width"] = i16(header_data, 0) file_info["width"] = i16(header_data, 0)
file_info["height"] = i16(header_data, 2) file_info["height"] = i16(header_data, 2)
@ -93,9 +94,14 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["compression"] = self.RAW file_info["compression"] = self.RAW
file_info["palette_padding"] = 3 file_info["palette_padding"] = 3
# --------------------------------------------- Windows Bitmap v2 to v5 # --------------------------------------------- Windows Bitmap v3 to v5
# v3, OS/2 v2, v4, v5 # 40: BITMAPINFOHEADER
elif file_info["header_size"] in (40, 64, 108, 124): # 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["y_flip"] = header_data[7] == 0xFF
file_info["direction"] = 1 if file_info["y_flip"] else -1 file_info["direction"] = 1 if file_info["y_flip"] else -1
file_info["width"] = i32(header_data, 0) file_info["width"] = i32(header_data, 0)
@ -117,10 +123,13 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["palette_padding"] = 4 file_info["palette_padding"] = 4
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
if file_info["compression"] == self.BITFIELDS: if file_info["compression"] == self.BITFIELDS:
if len(header_data) >= 52: masks = ["r_mask", "g_mask", "b_mask"]
for idx, mask in enumerate( if len(header_data) >= 48:
["r_mask", "g_mask", "b_mask", "a_mask"] 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) file_info[mask] = i32(header_data, 36 + idx * 4)
else: else:
# 40 byte headers only have the three components in the # 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, # location, but it is listed as a reserved component,
# and it is not generally an alpha channel # and it is not generally an alpha channel
file_info["a_mask"] = 0x0 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[mask] = i32(read(4))
file_info["rgb_mask"] = ( file_info["rgb_mask"] = (
file_info["r_mask"], file_info["r_mask"],
@ -175,9 +184,11 @@ class BmpImageFile(ImageFile.ImageFile):
32: [ 32: [
(0xFF0000, 0xFF00, 0xFF, 0x0), (0xFF0000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0), (0xFF000000, 0xFF0000, 0xFF00, 0x0),
(0xFF000000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0xFF), (0xFF000000, 0xFF0000, 0xFF00, 0xFF),
(0xFF, 0xFF00, 0xFF0000, 0xFF000000), (0xFF, 0xFF00, 0xFF0000, 0xFF000000),
(0xFF0000, 0xFF00, 0xFF, 0xFF000000), (0xFF0000, 0xFF00, 0xFF, 0xFF000000),
(0xFF000000, 0xFF00, 0xFF, 0xFF0000),
(0x0, 0x0, 0x0, 0x0), (0x0, 0x0, 0x0, 0x0),
], ],
24: [(0xFF0000, 0xFF00, 0xFF)], 24: [(0xFF0000, 0xFF00, 0xFF)],
@ -186,9 +197,11 @@ class BmpImageFile(ImageFile.ImageFile):
MASK_MODES = { MASK_MODES = {
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX", (32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR", (32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
(32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR", (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA", (32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA", (32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
(32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR", (24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16", (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""" """Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset # read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14) head_data = self.fp.read(14)
@ -291,7 +304,8 @@ class BmpRleDecoder(ImageFile.PyDecoder):
rle4 = self.args[1] rle4 = self.args[1]
data = bytearray() data = bytearray()
x = 0 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) pixels = self.fd.read(1)
byte = self.fd.read(1) byte = self.fd.read(1)
if not pixels or not byte: if not pixels or not byte:
@ -362,7 +376,7 @@ class DibImageFile(BmpImageFile):
format = "DIB" format = "DIB"
format_description = "Windows Bitmap" format_description = "Windows Bitmap"
def _open(self): def _open(self) -> None:
self._bitmap() self._bitmap()

View File

@ -29,7 +29,7 @@ def register_handler(handler):
# Image adapter # Image adapter
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
@ -37,7 +37,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
format = "BUFR" format = "BUFR"
format_description = "BUFR" format_description = "BUFR"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):

View File

@ -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" return prefix[:4] == b"\0\0\2\0"
@ -37,7 +37,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
format = "CUR" format = "CUR"
format_description = "Windows Cursor" format_description = "Windows Cursor"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
# check magic # check magic

View File

@ -29,7 +29,7 @@ from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return len(prefix) >= 4 and i32(prefix) == MAGIC return len(prefix) >= 4 and i32(prefix) == MAGIC
@ -63,7 +63,7 @@ class DcxImageFile(PcxImageFile):
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
self.seek(0) self.seek(0)
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.frame = frame self.frame = frame
@ -71,7 +71,7 @@ class DcxImageFile(PcxImageFile):
self.fp.seek(self._offset[frame]) self.fp.seek(self._offset[frame])
PcxImageFile._open(self) PcxImageFile._open(self)
def tell(self): def tell(self) -> int:
return self.frame return self.frame

View File

@ -271,16 +271,16 @@ class D3DFMT(IntEnum):
module = sys.modules[__name__] module = sys.modules[__name__]
for item in DDSD: for item in DDSD:
assert item.name is not None 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: for item1 in DDSCAPS:
assert item1.name is not None 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: for item2 in DDSCAPS2:
assert item2.name is not None 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: for item3 in DDPF:
assert item3.name is not None 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_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB DDS_RGB = DDPF.RGB
@ -331,7 +331,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS" format = "DDS"
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a DDS file" msg = "not a DDS file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -472,7 +472,7 @@ class DdsImageFile(ImageFile.ImageFile):
else: else:
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)] self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass
@ -497,7 +497,8 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
data = bytearray() data = bytearray()
bytecount = bitcount // 8 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") value = int.from_bytes(self.fd.read(bytecount), "little")
for i, mask in enumerate(masks): for i, mask in enumerate(masks):
masked_value = value & mask masked_value = value & mask
@ -505,7 +506,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
data += o8( data += o8(
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
) )
self.set_as_raw(bytes(data)) self.set_as_raw(data)
return -1, 0 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 " return prefix[:4] == b"DDS "

View File

@ -42,7 +42,7 @@ gs_binary: str | bool | None = None
gs_windows_binary = None gs_windows_binary = None
def has_ghostscript(): def has_ghostscript() -> bool:
global gs_binary, gs_windows_binary global gs_binary, gs_windows_binary
if gs_binary is None: if gs_binary is None:
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
@ -178,7 +178,7 @@ class PSFile:
self.char = None self.char = None
self.fp.seek(offset, whence) self.fp.seek(offset, whence)
def readline(self): def readline(self) -> str:
s = [self.char or b""] s = [self.char or b""]
self.char = None self.char = None
@ -195,7 +195,7 @@ class PSFile:
return b"".join(s).decode("latin-1") 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) 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"} mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
def _open(self): def _open(self) -> None:
(length, offset) = self._find_offset(self.fp) (length, offset) = self._find_offset(self.fp)
# go to offset - start of "%!PS" # go to offset - start of "%!PS"
@ -404,7 +404,7 @@ class EpsImageFile(ImageFile.ImageFile):
self.tile = [] self.tile = []
return Image.Image.load(self) 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 # we can't incrementally load, so force ImageFile.parser to
# use our custom load method by defining this method. # use our custom load method by defining this method.
pass pass

View File

@ -346,7 +346,7 @@ class Interop(IntEnum):
InteropVersion = 2 InteropVersion = 2
RelatedImageFileFormat = 4096 RelatedImageFileFormat = 4096
RelatedImageWidth = 4097 RelatedImageWidth = 4097
RleatedImageHeight = 4098 RelatedImageHeight = 4098
class IFD(IntEnum): class IFD(IntEnum):

View File

@ -10,6 +10,7 @@
# #
from __future__ import annotations from __future__ import annotations
import gzip
import math import math
from . import Image, ImageFile from . import Image, ImageFile
@ -27,14 +28,32 @@ class FitsImageFile(ImageFile.ImageFile):
assert self.fp is not None assert self.fp is not None
headers: dict[bytes, bytes] = {} headers: dict[bytes, bytes] = {}
header_in_progress = False
decoder_name = ""
while True: while True:
header = self.fp.read(80) header = self.fp.read(80)
if not header: if not header:
msg = "Truncated FITS file" msg = "Truncated FITS file"
raise OSError(msg) raise OSError(msg)
keyword = header[:8].strip() 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 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() value = header[8:].split(b"/")[0].strip()
if value.startswith(b"="): if value.startswith(b"="):
value = value[1:].strip() value = value[1:].strip()
@ -43,32 +62,87 @@ class FitsImageFile(ImageFile.ImageFile):
raise SyntaxError(msg) raise SyntaxError(msg)
headers[keyword] = value headers[keyword] = value
naxis = int(headers[b"NAXIS"]) if not decoder_name:
if naxis == 0:
msg = "No image data" msg = "No image data"
raise ValueError(msg) 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: if number_of_bits == 8:
self._mode = "L" self._mode = "L"
elif number_of_bits == 16: elif number_of_bits == 16:
self._mode = "I" self._mode = "I;16"
elif number_of_bits == 32: elif number_of_bits == 32:
self._mode = "I" self._mode = "I"
elif number_of_bits in (-32, -64): elif number_of_bits in (-32, -64):
self._mode = "F" self._mode = "F"
offset = math.ceil(self.fp.tell() / 2880) * 2880 args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,)
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))] 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 # Registry
Image.register_open(FitsImageFile.format, FitsImageFile, _accept) Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
Image.register_decoder("fits_gzip", FitsGzipDecoder)
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"]) Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])

View File

@ -27,7 +27,7 @@ from ._binary import o8
# decoder # decoder
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return ( return (
len(prefix) >= 6 len(prefix) >= 6
and i16(prefix, 4) in [0xAF11, 0xAF12] and i16(prefix, 4) in [0xAF11, 0xAF12]
@ -123,7 +123,7 @@ class FliImageFile(ImageFile.ImageFile):
palette[i] = (r, g, b) palette[i] = (r, g, b)
i += 1 i += 1
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
@ -132,7 +132,7 @@ class FliImageFile(ImageFile.ImageFile):
for f in range(self.__frame + 1, frame + 1): for f in range(self.__frame + 1, frame + 1):
self._seek(f) self._seek(f)
def _seek(self, frame): def _seek(self, frame: int) -> None:
if frame == 0: if frame == 0:
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
@ -162,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__offset += framesize self.__offset += framesize
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame

Some files were not shown because too many files have changed in this diff Show More