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}'
clone_folder: c:\pillow
init:
@ -27,7 +34,7 @@ install:
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.0.0.20230317
- choco install ghostscript --version=10.3.0
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |

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
ColumnLimit: 88
DerivePointerAlignment: false
IndentGotoLabels: false
IndentWidth: 4
Language: Cpp
PointerAlignment: Right

View File

@ -48,6 +48,21 @@ Thank you.
* Python:
* Pillow:
```text
Please paste here the output of running:
python3 -m PIL.report
or
python3 -m PIL --report
Or the output of the following Python code:
from PIL import report
# or
from PIL import features
features.pilinfo(supported_formats=False)
```
<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ jobs:
- name: Build and Run Valgrind
run: |
# The Pillow user in the docker container is UID 1000
sudo chown -R 1000 $GITHUB_WORKSPACE
# The Pillow user in the docker container is UID 1001
sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,84 @@
Changelog (Pillow)
==================
10.3.0 (unreleased)
10.4.0 (unreleased)
-------------------
- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978
[radarhere, hugovk]
- Fix ImagingAccess for I;16N on big-endian #7921
[Yay295, radarhere]
- Support reading P mode TIFF images with padding #7996
[radarhere]
- Deprecate support for libtiff < 4 #7998
[radarhere, hugovk]
- Corrected ImageShow UnixViewer command #7987
[radarhere]
- Use functools.cached_property in ImageStat #7952
[nulano, hugovk, radarhere]
- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956
[Cirras, radarhere]
- Support reading CMYK JPEG2000 images #7947
[radarhere]
10.3.0 (2024-04-01)
-------------------
- CVE-2024-28219: Use ``strncpy`` to avoid buffer overflow #7928
[radarhere, hugovk]
- Deprecate ``eval()``, replacing it with ``lambda_eval()`` and ``unsafe_eval()`` #7927
[radarhere, hugovk]
- Raise ``ValueError`` if seeking to greater than offset-sized integer in TIFF #7883
[radarhere]
- Add ``--report`` argument to ``__main__.py`` to omit supported formats #7818
[nulano, radarhere, hugovk]
- Added RGB to I;16, I;16L, I;16B and I;16N conversion #7918, #7920
[radarhere]
- Fix editable installation with custom build backend and configuration options #7658
[nulano, radarhere]
- Fix putdata() for I;16N on big-endian #7209
[Yay295, hugovk, radarhere]
- Determine MPO size from markers, not EXIF data #7884
[radarhere]
- Improved conversion from RGB to RGBa, LA and La #7888
[radarhere]
- Support FITS images with GZIP_1 compression #7894
[radarhere]
- Use I;16 mode for 9-bit JPEG 2000 images #7900
[scaramallion, radarhere]
- Raise ValueError if kmeans is negative #7891
[radarhere]
- Remove TIFF tag OSUBFILETYPE when saving using libtiff #7893
[radarhere]
- Raise ValueError for negative values when loading P1-P3 PPM images #7882
[radarhere]
- Added reading of JPEG2000 palettes #7870
[radarhere]
- Added alpha_quality argument when saving WebP images #7872
[radarhere]
- Fixed joined corners for ImageDraw rounded_rectangle() non-integer dimensions #7881
[radarhere]
@ -4298,7 +4373,7 @@ Changelog (Pillow)
- Documentation changes, URL update, transpose, release checklist
[radarhere]
- Fixed saving to nonexistant files specified by pathlib.Path objects #1748 (fixes #1747)
- Fixed saving to nonexistent files specified by pathlib.Path objects #1748 (fixes #1747)
[radarhere]
- Round Image.crop arguments to the nearest integer #1745 (fixes #1744)
@ -7509,7 +7584,7 @@ The test suite includes 400 individual tests.
- A handbook is available (distributed separately).
- The coordinate system is changed so that (0,0) is now located
in the upper left corner. This is in compliancy with ISO 12087
in the upper left corner. This is in compliance with ISO 12087
and 90% of all other image processing and graphics libraries.
- Modes "1" (bilevel) and "P" (palette) have been introduced. Note

View File

@ -1,11 +1,11 @@
The Python Imaging Library (PIL) is
Copyright © 1997-2011 by Secret Labs AB
Copyright © 1995-2011 by Fredrik Lundh
Copyright © 1995-2011 by Fredrik Lundh and contributors
Pillow is the friendly PIL fork. It is
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
Like PIL, Pillow is licensed under the open source HPND License:

View File

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

View File

@ -6,9 +6,9 @@
## Python Imaging Library (Fork)
Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and
Pillow is the friendly PIL fork by [Jeffrey A. Clark and
contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
PIL is the Python Imaging Library by Fredrik Lundh and contributors.
As of 2019, Pillow development is
[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
@ -101,7 +101,7 @@ The core image library is designed for fast access to data stored in a few basic
## More Information
- [Documentation](https://pillow.readthedocs.io/)
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
- [Issues](https://github.com/python-pillow/Pillow/issues)

View File

@ -20,8 +20,10 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0
git push --tags
```
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
```bash
@ -50,7 +52,9 @@ Released as needed for security, installation or critical bug fixes.
```bash
make sdist
```
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash
git push
@ -72,18 +76,14 @@ Released as needed privately to individual vendors for critical security-related
git tag 2.5.3
git push origin --tags
```
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash
git push origin 2.5.x
```
## Source and Binary Distributions
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag.
## Publicize Release
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321

View File

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

View File

@ -11,6 +11,7 @@ import subprocess
import sys
import sysconfig
import tempfile
from functools import lru_cache
from io import BytesIO
from typing import Any, Callable, Sequence
@ -114,7 +115,9 @@ def assert_image_similar(
diff = 0
for ach, bch in zip(a.split(), b.split()):
chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L")
chdiff = ImageMath.lambda_eval(
lambda args: abs(args["a"] - args["b"]), a=ach, b=bch
).convert("L")
diff += sum(i * num for i, num in enumerate(chdiff.histogram()))
ave_diff = diff / (a.size[0] * a.size[1])
@ -250,25 +253,38 @@ def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
return out.getvalue()
def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
def hopper(mode: str | None = None) -> Image.Image:
# Use caching to reduce reading from disk, but return a copy
# so that the cached image isn't modified by the tests
# (for fast, isolated, repeatable tests).
if mode is None:
# Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors
# what we should catch.
# Operations on not-yet-loaded images are a separate class of errors
# that we should catch.
return Image.open("Tests/images/hopper.ppm")
# Use caching to reduce reading from disk but so an original copy is
# returned each time and the cached image isn't modified by tests
# (for fast, isolated, repeatable tests).
im = cache.get(mode)
if im is None:
if mode == "F":
im = hopper("L").convert(mode)
elif mode[:4] == "I;16":
im = hopper("I").convert(mode)
else:
im = hopper().convert(mode)
cache[mode] = im
return im.copy()
return _cached_hopper(mode).copy()
@lru_cache
def _cached_hopper(mode: str) -> Image.Image:
if mode == "F":
im = hopper("L")
else:
im = hopper()
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
im = im.convert(mode)
else:
try:
im = im.convert(mode)
except ImportError:
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
return im
def djpeg_available() -> bool:

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

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",
"pal8rletrns.bmp",
"rgb32bf-xbgr.bmp",
"rgba32.bmp",
"rgb32h52.bmp",
"rgba32h56.bmp",
]
for f in get_files("q"):
try:

View File

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

View File

@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from PIL import BmpImagePlugin, Image
from PIL import BmpImagePlugin, Image, _binary
from .helper import (
assert_image_equal,
@ -128,6 +128,29 @@ def test_load_dib() -> None:
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png")
@pytest.mark.parametrize(
"header_size, path",
(
(12, "g/pal8os2.bmp"),
(40, "g/pal1.bmp"),
(52, "q/rgb32h52.bmp"),
(56, "q/rgba32h56.bmp"),
(64, "q/pal8os2v2.bmp"),
(108, "g/pal8v4.bmp"),
(124, "g/pal8v5.bmp"),
),
)
def test_dib_header_size(header_size, path):
image_path = "Tests/images/bmp/" + path
with open(image_path, "rb") as fp:
data = fp.read()[14:]
assert _binary.i32le(data) == header_size
dib = io.BytesIO(data)
with Image.open(dib) as im:
im.load()
def test_save_dib(tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.dib")

View File

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

View File

@ -6,7 +6,7 @@ import pytest
from PIL import FitsImagePlugin, Image
from .helper import assert_image_equal, hopper
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_FILE = "Tests/images/hopper.fits"
@ -22,6 +22,11 @@ def test_open() -> None:
assert_image_equal(im, hopper("L"))
def test_gzip1() -> None:
with Image.open("Tests/images/m13_gzip.fits") as im:
assert_image_equal_tofile(im, "Tests/images/m13.fits")
def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"

View File

@ -825,7 +825,7 @@ class TestFileJpeg:
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
# Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated."
# https://exiv2.org/tags.html
# https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self) -> None:

View File

@ -289,6 +289,16 @@ def test_rgba(ext: str) -> None:
assert im.mode == "RGBA"
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@skip_unless_feature_version("jpg_2000", "2.5.1")
def test_cmyk() -> None:
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im:
assert im.mode == "CMYK"
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im:
@ -364,6 +374,16 @@ def test_subsampling_decode(name: str) -> None:
assert_image_similar(im, expected, epsilon)
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_pclr() -> None:
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
assert im.mode == "P"
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
@ -436,3 +456,9 @@ def test_plt_marker() -> None:
hdr = out.read(2)
length = _binary.i16be(hdr)
out.seek(length - 2, os.SEEK_CUR)
def test_9bit():
with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16"
assert im.size == (128, 128)

View File

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

39
Tests/test_file_mpeg.py Normal file
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:
# This image has been hexedited to contain a different size
# in the EXIF data of the second frame
# in the SOF marker of the second frame
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
assert im.size == (640, 480)

View File

@ -85,7 +85,9 @@ class TestFilePng:
def test_sanity(self, tmp_path: Path) -> None:
# internal version number
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
assert re.search(
r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib")
)
test_file = str(tmp_path / "temp.png")

View File

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

View File

@ -113,6 +113,10 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_seek_too_large(self):
with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif")
def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:

View File

@ -151,3 +151,15 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
target = im.convert("RGBA")
assert_image_similar(image, target, 25.0)
def test_alpha_quality(tmp_path: Path) -> None:
with Image.open("Tests/images/transparent.png") as im:
out = str(tmp_path / "temp.webp")
im.save(out)
out_quality = str(tmp_path / "quality.webp")
im.save(out_quality, alpha_quality=50)
with Image.open(out) as reloaded:
with Image.open(out_quality) as reloaded_quality:
assert reloaded.tobytes() != reloaded_quality.tobytes()

View File

@ -188,3 +188,21 @@ def test_seek_errors() -> None:
with pytest.raises(EOFError):
im.seek(42)
def test_alpha_quality(tmp_path: Path) -> None:
with Image.open("Tests/images/transparent.png") as im:
first_frame = Image.new("L", im.size)
out = str(tmp_path / "temp.webp")
first_frame.save(out, save_all=True, append_images=[im])
out_quality = str(tmp_path / "quality.webp")
first_frame.save(
out_quality, save_all=True, append_images=[im], alpha_quality=50
)
with Image.open(out) as reloaded:
reloaded.seek(1)
with Image.open(out_quality) as reloaded_quality:
reloaded_quality.seek(1)
assert reloaded.tobytes() != reloaded_quality.tobytes()

View File

@ -28,43 +28,26 @@ from .helper import (
assert_image_similar_tofile,
assert_not_all_same,
hopper,
is_big_endian,
is_win32,
mark_if_feature_version,
skip_unless_feature,
)
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
return Image.new(mode, size)
else:
return Image.new(mode, size)
class TestImage:
@pytest.mark.parametrize(
"mode",
(
"1",
"P",
"PA",
"L",
"LA",
"La",
"F",
"I",
"I;16",
"I;16L",
"I;16B",
"I;16N",
"RGB",
"RGBX",
"RGBA",
"RGBa",
"BGR;15",
"BGR;16",
"BGR;24",
"CMYK",
"YCbCr",
"LAB",
"HSV",
),
)
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_image_modes_success(self, mode: str) -> None:
Image.new(mode, (1, 1))
helper_image_new(mode, (1, 1))
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None:
@ -1042,6 +1025,38 @@ class TestImage:
assert im.fp is None
class TestImageBytes:
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
reloaded = Image.frombytes(mode, im.size, source_bytes)
else:
reloaded = Image.frombytes(mode, im.size, source_bytes)
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_roundtrip_bytes_method(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()
reloaded = helper_image_new(mode, im.size)
reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_getdata_putdata(self, mode: str) -> None:
if is_big_endian() and mode == "BGR;15":
pytest.xfail("Known failure of BGR;15 on big-endian")
im = hopper(mode)
reloaded = helper_image_new(mode, im.size)
reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded)
class MockEncoder(ImageFile.PyEncoder):
pass

View File

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

View File

@ -91,6 +91,16 @@ def test_fromarray() -> None:
Image.fromarray(wrapped)
def test_fromarray_strides_without_tobytes() -> None:
class Wrapper:
def __init__(self, arr_params: dict[str, Any]) -> None:
self.__array_interface__ = arr_params
with pytest.raises(ValueError):
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
Image.fromarray(wrapped, "L")
def test_fromarray_palette() -> None:
# Arrange
i = im.convert("L")

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

View File

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

View File

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

View File

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

View File

@ -94,6 +94,19 @@ def test_quantize_dither_diff() -> None:
assert dither.tobytes() != nodither.tobytes()
@pytest.mark.parametrize(
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
)
def test_quantize_kmeans(method) -> None:
im = hopper()
no_kmeans = im.quantize(kmeans=0, method=method)
kmeans = im.quantize(kmeans=1, method=method)
assert kmeans.tobytes() != no_kmeans.tobytes()
with pytest.raises(ValueError):
im.quantize(kmeans=-1, method=method)
def test_colors() -> None:
im = hopper()
colors = 2

View File

@ -186,7 +186,9 @@ def assert_compare_images(
bands = ImageMode.getmode(a.mode).bands
for band, ach, bch in zip(bands, a.split(), b.split()):
ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch)
ch_diff = ImageMath.lambda_eval(
lambda args: args["convert"](abs(args["a"] - args["b"]), "L"), a=ach, b=bch
)
ch_hist = ch_diff.histogram()
average_diff = sum(i * num for i, num in enumerate(ch_hist)) / (

View File

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

View File

@ -4,13 +4,14 @@ import datetime
import os
import re
import shutil
import sys
from io import BytesIO
from pathlib import Path
from typing import Any
import pytest
from PIL import Image, ImageMode, features
from PIL import Image, ImageMode, ImageWin, features
from .helper import (
assert_image,
@ -18,6 +19,7 @@ from .helper import (
assert_image_similar,
assert_image_similar_tofile,
hopper,
is_pypy,
)
try:
@ -213,6 +215,10 @@ def test_display_profile() -> None:
# try fetching the profile for the current display device
ImageCms.get_display_profile()
if sys.platform == "win32":
ImageCms.get_display_profile(ImageWin.HDC(0))
ImageCms.get_display_profile(ImageWin.HWND(0))
def test_lab_color_profile() -> None:
ImageCms.createProfile("LAB", 5000)
@ -496,16 +502,34 @@ def test_non_ascii_path(tmp_path: Path) -> None:
def test_profile_typesafety() -> None:
"""Profile init type safety
prepatch, these would segfault, postpatch they should emit a typeerror
"""
# does not segfault
with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(0).tobytes()
with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(1).tobytes()
# also check core function
with pytest.raises(TypeError):
ImageCms.core.profile_tobytes(0)
with pytest.raises(TypeError):
ImageCms.core.profile_tobytes(1)
if not is_pypy():
# core profile should not be directly instantiable
with pytest.raises(TypeError):
ImageCms.core.CmsProfile()
with pytest.raises(TypeError):
ImageCms.core.CmsProfile(0)
@pytest.mark.skipif(is_pypy(), reason="fails on PyPy")
def test_transform_typesafety() -> None:
# core transform should not be directly instantiable
with pytest.raises(TypeError):
ImageCms.core.CmsTransform()
with pytest.raises(TypeError):
ImageCms.core.CmsTransform(0)
def assert_aux_channel_preserved(
mode: str, transform_in_place: bool, preserved_channel: str
@ -637,6 +661,11 @@ def test_auxiliary_channels_isolated() -> None:
assert_image_equal(test_image.convert(dst_format[2]), reference_image)
def test_long_modes() -> None:
p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1))

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)
def _test_multipage_tiff() -> None:
@pytest.mark.parametrize(
"libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
)
def test_multipage_tiff(monkeypatch: pytest.MonkeyPatch, libtiff: bool) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", libtiff)
with Image.open("Tests/images/multipage.tiff") as im:
for index, frame in enumerate(ImageSequence.Iterator(im)):
frame.load()
@ -55,17 +59,6 @@ def _test_multipage_tiff() -> None:
frame.convert("RGB")
def test_tiff() -> None:
_test_multipage_tiff()
@skip_unless_feature("libtiff")
def test_libtiff() -> None:
TiffImagePlugin.READ_LIBTIFF = True
_test_multipage_tiff()
TiffImagePlugin.READ_LIBTIFF = False
def test_consecutive() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
first_frame = None

View File

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

View File

@ -19,7 +19,7 @@ from PIL import Image
# 7
# 160
# one of string.whitespace is not freely convertable into ascii.
# one of string.whitespace is not freely convertible into ascii.
path = "Tests/images/hopper.jpg"

View File

@ -4,9 +4,16 @@ import os
import subprocess
import sys
import pytest
def test_main() -> None:
out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8")
@pytest.mark.parametrize(
"args, report",
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
)
def test_main(args, report) -> None:
args = [sys.executable, "-m"] + args
out = subprocess.check_output(args).decode("utf-8")
lines = out.splitlines()
assert lines[0] == "-" * 68
assert lines[1].startswith("Pillow ")
@ -15,9 +22,15 @@ def test_main() -> None:
while lines[0].startswith(" "):
lines = lines[1:]
assert lines[0] == "-" * 68
assert lines[1].startswith("Python modules loaded from ")
assert lines[2].startswith("Binary modules loaded from ")
assert lines[3] == "-" * 68
assert lines[1].startswith("Python executable is")
lines = lines[2:]
if lines[0].startswith("Environment Python files loaded from"):
lines = lines[1:]
assert lines[0].startswith("System Python files loaded from")
assert lines[1] == "-" * 68
assert lines[2].startswith("Python Pillow modules loaded from ")
assert lines[3].startswith("Binary Pillow modules loaded from ")
assert lines[4] == "-" * 68
jpeg = (
os.linesep
+ "-" * 68
@ -31,4 +44,4 @@ def test_main() -> None:
+ "-" * 68
+ os.linesep
)
assert jpeg in out
assert report == (jpeg not in out)

View File

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

View File

@ -11,41 +11,12 @@ backend_class = build_wheel.__self__.__class__
class _CustomBuildMetaBackend(backend_class):
def run_setup(self, setup_script="setup.py"):
if self.config_settings:
for key, values in self.config_settings.items():
if not isinstance(values, list):
values = [values]
for value in values:
sys.argv.append(f"--pillow-configuration={key}={value}")
def config_has(key, value):
settings = self.config_settings.get(key)
if settings:
if not isinstance(settings, list):
settings = [settings]
return value in settings
flags = []
for dependency in (
"zlib",
"jpeg",
"tiff",
"freetype",
"raqm",
"lcms",
"webp",
"webpmux",
"jpeg2000",
"imagequant",
"xcb",
):
if config_has(dependency, "enable"):
flags.append("--enable-" + dependency)
elif config_has(dependency, "disable"):
flags.append("--disable-" + dependency)
for dependency in ("raqm", "fribidi"):
if config_has(dependency, "vendor"):
flags.append("--vendor-" + dependency)
if self.config_settings.get("platform-guessing") == "disable":
flags.append("--disable-platform-guessing")
if self.config_settings.get("debug") == "true":
flags.append("--debug")
if flags:
sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:]
return super().run_setup(setup_script)
def build_wheel(
@ -54,5 +25,15 @@ class _CustomBuildMetaBackend(backend_class):
self.config_settings = config_settings
return super().build_wheel(wheel_directory, config_settings, metadata_directory)
def build_editable(
self, wheel_directory, config_settings=None, metadata_directory=None
):
self.config_settings = config_settings
return super().build_editable(
wheel_directory, config_settings, metadata_directory
)
build_wheel = _CustomBuildMetaBackend().build_wheel
_backend = _CustomBuildMetaBackend()
build_wheel = _backend.build_wheel
build_editable = _backend.build_editable

View File

@ -1,11 +1,11 @@
The Python Imaging Library (PIL) is
Copyright © 1997-2011 by Secret Labs AB
Copyright © 1995-2011 by Fredrik Lundh
Copyright © 1995-2011 by Fredrik Lundh and contributors
Pillow is the friendly PIL fork. It is
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
Like PIL, Pillow is licensed under the open source PIL
Software License:

View File

@ -46,7 +46,7 @@ clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph
.PHONY: html
html:

View File

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

48
docs/dater.py Normal file
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__``
============================================ ====================================================
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
----------------

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;16B`` (16-bit big endian unsigned integer pixels)
* ``I;16N`` (16-bit native endian unsigned integer pixels)
* ``BGR;15`` (15-bit reversed true colour)
* ``BGR;16`` (16-bit reversed true colour)
* ``BGR;24`` (24-bit reversed true colour)
Premultiplied alpha is where the values for each other channel have been
multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)``

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.
**quality**
Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest
Integer, 0-100, defaults to 80. For lossy, 0 gives the smallest
size and 100 the largest. For lossless, this parameter is the amount
of effort put into the compression: 0 is the fastest, but gives larger
files compared to the slowest, but best, 100.
**alpha_quality**
Integer, 0-100, defaults to 100. For lossy compression only. 0 gives the
smallest size and 100 is lossless.
**method**
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4.
@ -1335,7 +1339,8 @@ FITS
.. versionadded:: 9.1.0
Pillow identifies and reads FITS files, commonly used for astronomy.
Pillow identifies and reads FITS files, commonly used for astronomy. Uncompressed and
GZIP_1 compressed images can be read.
FLI, FLC
^^^^^^^^
@ -1351,9 +1356,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
FPX
^^^
Pillow reads Kodak FlashPix files. In the current version, only the highest
resolution image is read from the file, and the viewing transform is not taken
into account.
Pillow reads Kodak FlashPix files. Only the highest resolution image is read from the
file, and the viewing transform is not taken into account.
To enable FPX support, you must install :pypi:`olefile`.
@ -1484,7 +1488,9 @@ QOI
.. versionadded:: 9.5.0
Pillow identifies and reads images in Quite OK Image format.
Pillow reads images in Quite OK Image format using a Python decoder. If you wish to
write code specifically for this format, :pypi:`qoi` is an alternative library that
uses C to decode the image and interfaces with NumPy.
SUN
^^^

View File

@ -1,7 +1,7 @@
Pillow
======
Pillow is the friendly PIL fork by `Jeffrey A. Clark (Alex) and contributors <https://github.com/python-pillow/Pillow/graphs/contributors>`_. PIL is the Python Imaging Library by Fredrik Lundh and contributors.
Pillow is the friendly PIL fork by `Jeffrey A. Clark and contributors <https://github.com/python-pillow/Pillow/graphs/contributors>`_. PIL is the Python Imaging Library by Fredrik Lundh and contributors.
Pillow for enterprise is available via the Tidelift Subscription. `Learn more <https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=docs&utm_campaign=enterprise>`_.

View File

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

View File

@ -25,23 +25,19 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Arch | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS 7 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 8 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 12 Monterey | 3.8, 3.9 | x86-64 |
| macOS 13 Ventura | 3.8, 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | PyPy3 | |
@ -51,7 +47,9 @@ These platforms are built and tested for every change.
| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.10 | arm64v8, ppc64le, |
| | 3.10 | arm64v8 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.8 | x86-64 |
@ -81,7 +79,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0 |arm |
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +----------------------------+------------------+ |

View File

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

View File

@ -73,7 +73,7 @@ can be easily displayed in a chromaticity diagram, for example).
:canonical: PIL._imagingcms.CmsProfile
.. py:attribute:: creation_date
:type: Optional[datetime.datetime]
:type: datetime.datetime | None
Date and time this profile was first created (see 7.2.1 of ICC.1:2010).
@ -156,58 +156,58 @@ can be easily displayed in a chromaticity diagram, for example).
not been calculated (see 7.2.18 of ICC.1:2010).
.. py:attribute:: copyright
:type: Optional[str]
:type: str | None
The text copyright information for the profile (see 9.2.21 of ICC.1:2010).
.. py:attribute:: manufacturer
:type: Optional[str]
:type: str | None
The (English) display string for the device manufacturer (see
9.2.22 of ICC.1:2010).
.. py:attribute:: model
:type: Optional[str]
:type: str | None
The (English) display string for the device model of the device
for which this profile is created (see 9.2.23 of ICC.1:2010).
.. py:attribute:: profile_description
:type: Optional[str]
:type: str | None
The (English) display string for the profile description (see
9.2.41 of ICC.1:2010).
.. py:attribute:: target
:type: Optional[str]
:type: str | None
The name of the registered characterization data set, or the
measurement data for a characterization target (see 9.2.14 of
ICC.1:2010).
.. py:attribute:: red_colorant
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: green_colorant
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: blue_colorant
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: luminance
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
The absolute luminance of emissive devices in candelas per square
metre as described by the Y channel (see 9.2.32 of ICC.1:2010).
@ -215,7 +215,7 @@ can be easily displayed in a chromaticity diagram, for example).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: chromaticity
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]] | None
The data of the phosphor/colorant chromaticity set used (red,
green and blue channels, see 9.2.16 of ICC.1:2010).
@ -223,7 +223,7 @@ can be easily displayed in a chromaticity diagram, for example).
The value is in the format ``((x, y, Y), (x, y, Y), (x, y, Y))``, if available.
.. py:attribute:: chromatic_adaption
:type: tuple[tuple[float]]
:type: tuple[tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]], tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]]] | None
The chromatic adaption matrix converts a color measured using the
actual illumination conditions and relative to the actual adopted
@ -249,34 +249,34 @@ can be easily displayed in a chromaticity diagram, for example).
9.2.19 of ICC.1:2010).
.. py:attribute:: colorimetric_intent
:type: Optional[str]
:type: str | None
4-character string (padded with whitespace) identifying the image
state of PCS colorimetry produced using the colorimetric intent
transforms (see 9.2.20 of ICC.1:2010 for details).
.. py:attribute:: perceptual_rendering_intent_gamut
:type: Optional[str]
:type: str | None
4-character string (padded with whitespace) identifying the (one)
standard reference medium gamut (see 9.2.37 of ICC.1:2010 for
details).
.. py:attribute:: saturation_rendering_intent_gamut
:type: Optional[str]
:type: str | None
4-character string (padded with whitespace) identifying the (one)
standard reference medium gamut (see 9.2.37 of ICC.1:2010 for
details).
.. py:attribute:: technology
:type: Optional[str]
:type: str | None
4-character string (padded with whitespace) identifying the device
technology (see 9.2.47 of ICC.1:2010 for details).
.. py:attribute:: media_black_point
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
This tag specifies the media black point and is used for
generating absolute colorimetry.
@ -287,19 +287,19 @@ can be easily displayed in a chromaticity diagram, for example).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: media_white_point_temperature
:type: Optional[float]
:type: float | None
Calculates the white point temperature (see the LCMS documentation
for more information).
.. py:attribute:: viewing_condition
:type: Optional[str]
:type: str | None
The (English) display string for the viewing conditions (see
9.2.48 of ICC.1:2010).
.. py:attribute:: screening_description
:type: Optional[str]
:type: str | None
The (English) display string for the screening conditions.
@ -307,21 +307,21 @@ can be easily displayed in a chromaticity diagram, for example).
version 4.
.. py:attribute:: red_primary
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
The XYZ-transformed of the RGB primary color red (1, 0, 0).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: green_primary
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
The XYZ-transformed of the RGB primary color green (0, 1, 0).
The value is in the format ``((X, Y, Z), (x, y, Y))``, if available.
.. py:attribute:: blue_primary
:type: Optional[tuple[tuple[float]]]
:type: tuple[tuple[float, float, float], tuple[float, float, float]] | None
The XYZ-transformed of the RGB primary color blue (0, 0, 1).
@ -334,7 +334,7 @@ can be easily displayed in a chromaticity diagram, for example).
documentation on LCMS).
.. py:attribute:: clut
:type: dict[tuple[bool]]
:type: dict[int, tuple[bool, bool, bool]] | None
Returns a dictionary of all supported intents and directions for
the CLUT model.
@ -353,7 +353,7 @@ can be easily displayed in a chromaticity diagram, for example).
that intent is supported for that direction.
.. py:attribute:: intent_supported
:type: dict[tuple[bool]]
:type: dict[int, tuple[bool, bool, bool]] | None
Returns a dictionary of all supported intents and directions.
@ -372,7 +372,7 @@ can be easily displayed in a chromaticity diagram, for example).
There is one function defined on the class:
.. py:method:: is_intent_supported(intent, direction)
.. py:method:: is_intent_supported(intent: int, direction: int, /)
Returns if the intent is supported for the given direction.

View File

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

View File

@ -4,9 +4,12 @@
:py:mod:`~PIL.ImageMath` Module
===============================
The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”. The
module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes
an expression string and one or more images.
The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that
can take a number of images and generate a result.
:py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band
images, use the :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge`
function.
Example: Using the :py:mod:`~PIL.ImageMath` module
--------------------------------------------------
@ -17,35 +20,69 @@ Example: Using the :py:mod:`~PIL.ImageMath` module
with Image.open("image1.jpg") as im1:
with Image.open("image2.jpg") as im2:
out = ImageMath.lambda_eval(
lambda args: args["convert"](args["min"](args["a"], args["b"]), 'L'),
a=im1,
b=im2
)
out = ImageMath.unsafe_eval(
"convert(min(a, b), 'L')",
a=im1,
b=im2
)
out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2)
out.save("result.png")
.. py:function:: lambda_eval(expression, options)
.. py:function:: eval(expression, environment)
Returns the result of an image function.
Evaluate expression in the given environment.
:param expression: A function that receives a dictionary.
:param options: Values to add to the function's dictionary, mapping image
names to Image instances. You can use one or more keyword
arguments instead of a dictionary, as shown in the above
example. Note that the names must be valid Python
identifiers.
:return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression.
In the current version, :py:mod:`~PIL.ImageMath` only supports
single-layer images. To process multi-band images, use the
:py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge`
function.
.. py:function:: unsafe_eval(expression, options)
Evaluates an image expression.
.. danger::
This uses Python's ``eval()`` function to process the expression string,
and carries the security risks of doing so. It is not
recommended to process expressions without considering this.
:py:meth:`lambda_eval` is a more secure alternative.
:py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band
images, use the :py:meth:`~PIL.Image.Image.split` method or
:py:func:`~PIL.Image.merge` function.
:param expression: A string which uses the standard Python expression
syntax. In addition to the standard operators, you can
also use the functions described below.
:param environment: A dictionary that maps image names to Image instances.
You can use one or more keyword arguments instead of a
dictionary, as shown in the above example. Note that
the names must be valid Python identifiers.
:param options: Values to add to the function's dictionary, mapping image
names to Image instances. You can use one or more keyword
arguments instead of a dictionary, as shown in the above
example. Note that the names must be valid Python
identifiers.
:return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression.
Expression syntax
-----------------
Expressions are standard Python expressions, but theyre evaluated in a
non-standard environment. You can use PIL methods as usual, plus the following
set of operators and functions:
* :py:meth:`lambda_eval` expressions are functions that receive a dictionary
containing images and operators.
* :py:meth:`unsafe_eval` expressions are standard Python expressions,
but 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
^^^^^^^^^^^^^^^^^^

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

View File

@ -7,67 +7,6 @@
The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or
for a region of an image.
.. py:class:: Stat(image_or_list, mask=None)
Calculate statistics for the given image. If a mask is included,
only the regions covered by that mask are included in the
statistics. You can also pass in a previously calculated histogram.
:param image: A PIL image, or a precalculated histogram.
.. note::
For a PIL image, calculations rely on the
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
grouped into 256 bins, even if the image has more than 8 bits per
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
of more than 255.
:param mask: An optional mask.
.. py:attribute:: extrema
Min/max values for each band in the image.
.. note::
This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
simply returns the low and high bins used. This is correct for
images with 8 bits per channel, but fails for other modes such as
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
return per-band extrema for the image. This is more correct and
efficient because, for non-8-bit modes, the histogram method uses
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
.. py:attribute:: count
Total number of pixels for each band in the image.
.. py:attribute:: sum
Sum of all pixels for each band in the image.
.. py:attribute:: sum2
Squared sum of all pixels for each band in the image.
.. py:attribute:: mean
Average (arithmetic mean) pixel level for each band in the image.
.. py:attribute:: median
Median pixel level for each band in the image.
.. py:attribute:: rms
RMS (root-mean-square) for each band in the image.
.. py:attribute:: var
Variance for each band in the image.
.. py:attribute:: stddev
Standard deviation for each band in the image.
.. autoclass:: Stat
:members:
:special-members: __init__

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
``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute
``environment`` argument of :py:meth:`!PIL.ImageMath.eval`, they may be able to execute
arbitrary code. To prevent this, keys matching the names of builtins and keys
containing double underscores will now raise a :py:exc:`ValueError`.

View File

@ -4,21 +4,21 @@
Security
========
TODO
^^^^
ImageMath eval()
^^^^^^^^^^^^^^^^
TODO
.. danger::
``ImageMath.eval()`` uses Python's ``eval()`` function to process the expression
string, and carries the security risks of doing so. A direct replacement for this is
the new :py:meth:`~PIL.ImageMath.unsafe_eval`, but that carries the same risks. It is
not recommended to process expressions without considering this.
:py:meth:`~PIL.ImageMath.lambda_eval` is a more secure alternative.
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
:cve:`2024-28219`: Fix buffer overflow in ``_imagingcms.c``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
In ``_imagingcms.c``, two ``strcpy`` calls were able to copy too much data into fixed
length strings. This has been fixed by using ``strncpy`` instead.
Deprecations
============
@ -58,13 +58,34 @@ Deprecated Use instead
:py:data:`sys.version_info`, and ``PIL.__version__``
============================================ ====================================================
ImageMath.eval()
^^^^^^^^^^^^^^^^
``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or
:py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more
information.
API Changes
===========
TODO
^^^^
Added alpha_quality argument when saving WebP images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
When saving WebP images, an ``alpha_quality`` argument can be passed to the encoder. It
is an integer value between 0 to 100, where values other than 100 will provide lossy
compression.
Negative kmeans error
^^^^^^^^^^^^^^^^^^^^^
When calling :py:meth:`~PIL.Image.Image.quantize`, a negative ``kmeans`` will now
raise a :py:exc:`ValueError`, unless a palette is supplied to make the value redundant.
Negative P1-P3 PPM value error
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If a P1-P3 PPM image contains a negative value, a :py:exc:`ValueError` will now be
raised.
API Additions
=============
@ -90,3 +111,9 @@ Release GIL when fetching WebP frames
Python's Global Interpreter Lock is now released when fetching WebP frames from
the libwebp decoder.
Type hints
^^^^^^^^^^
Pillow now has type hints for a large part of its modules, and the package
includes a ``py.typed`` file and the ``Typing :: Typed`` Trove classifier.

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
will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will
will now restrict the builtins available to :py:meth:`!PIL.ImageMath.eval`. This will
help prevent problems arising if users evaluate arbitrary expressions, such as
``ImageMath.eval("exec(exit())")``.

View File

@ -18,7 +18,7 @@ has been present since PIL.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
While Pillow 9.0 restricted top-level builtins available to
:py:meth:`PIL.ImageMath.eval`, it did not prevent builtins
:py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins
available to lambda expressions. These are now also restricted.
Other Changes

View File

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

View File

@ -15,7 +15,7 @@ keywords = [
"Imaging",
]
license = {text = "HPND"}
authors = [{name = "Jeffrey A. Clark (Alex)", email = "aclark@aclark.net"}]
authors = [{name = "Jeffrey A. Clark", email = "aclark@aclark.net"}]
requires-python = ">=3.8"
classifiers = [
"Development Status :: 6 - Mature",
@ -42,10 +42,9 @@ dynamic = [
docs = [
"furo",
"olefile",
"sphinx>=2.4",
"sphinx>=7.3",
"sphinx-copybutton",
"sphinx-inline-tabs",
"sphinx-removed-in",
"sphinxext-opengraph",
]
fpx = [
@ -96,6 +95,9 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"
[tool.ruff]
fix = true
[tool.ruff.lint]
select = [
"C4", # flake8-comprehensions
@ -106,6 +108,7 @@ select = [
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa)
"UP", # pyupgrade
"W", # pycodestyle warnings
@ -116,6 +119,8 @@ ignore = [
"E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ','
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
]
[tool.ruff.lint.per-file-ignores]

View File

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

View File

@ -23,8 +23,10 @@ from setuptools.command.build_ext import build_ext
def get_version():
version_file = "src/PIL/_version.py"
with open(version_file, encoding="utf-8") as f:
exec(compile(f.read(), version_file, "exec"))
return locals()["__version__"]
return f.read().split('"')[1]
configuration = {}
PILLOW_VERSION = get_version()
@ -334,15 +336,24 @@ class pil_build_ext(build_ext):
+ [("add-imaging-libs=", None, "Add libs to _imaging build")]
)
@staticmethod
def check_configuration(option, value):
return True if value in configuration.get(option, []) else None
def initialize_options(self):
self.disable_platform_guessing = None
self.disable_platform_guessing = self.check_configuration(
"platform-guessing", "disable"
)
self.add_imaging_libs = ""
build_ext.initialize_options(self)
for x in self.feature:
setattr(self, f"disable_{x}", None)
setattr(self, f"enable_{x}", None)
setattr(self, f"disable_{x}", self.check_configuration(x, "disable"))
setattr(self, f"enable_{x}", self.check_configuration(x, "enable"))
for x in ("raqm", "fribidi"):
setattr(self, f"vendor_{x}", None)
setattr(self, f"vendor_{x}", self.check_configuration(x, "vendor"))
if self.check_configuration("debug", "true"):
self.debug = True
self.parallel = configuration.get("parallel", [None])[-1]
def finalize_options(self):
build_ext.finalize_options(self)
@ -1007,6 +1018,12 @@ ext_modules = [
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
]
# parse configuration from _custom_build/backend.py
while sys.argv[-1].startswith("--pillow-configuration="):
_, key, value = sys.argv.pop().split("=", 2)
configuration.setdefault(key, []).append(value)
try:
setup(
cmdclass={"build_ext": pil_build_ext},
@ -1020,7 +1037,7 @@ The headers or library files could not be found for {str(err)},
a required dependency when compiling Pillow from source.
Please see the install instructions at:
https://pillow.readthedocs.io/en/latest/installation.html
https://pillow.readthedocs.io/en/latest/installation/basic-installation.html
"""
sys.stderr.write(msg)

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@
#
from __future__ import annotations
import gzip
import math
from . import Image, ImageFile
@ -27,14 +28,32 @@ class FitsImageFile(ImageFile.ImageFile):
assert self.fp is not None
headers: dict[bytes, bytes] = {}
header_in_progress = False
decoder_name = ""
while True:
header = self.fp.read(80)
if not header:
msg = "Truncated FITS file"
raise OSError(msg)
keyword = header[:8].strip()
if keyword == b"END":
if keyword in (b"SIMPLE", b"XTENSION"):
header_in_progress = True
elif headers and not header_in_progress:
# This is now a data unit
break
elif keyword == b"END":
# Seek to the end of the header unit
self.fp.seek(math.ceil(self.fp.tell() / 2880) * 2880)
if not decoder_name:
decoder_name, offset, args = self._parse_headers(headers)
header_in_progress = False
continue
if decoder_name:
# Keep going to read past the headers
continue
value = header[8:].split(b"/")[0].strip()
if value.startswith(b"="):
value = value[1:].strip()
@ -43,32 +62,87 @@ class FitsImageFile(ImageFile.ImageFile):
raise SyntaxError(msg)
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
if not decoder_name:
msg = "No image data"
raise ValueError(msg)
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
number_of_bits = int(headers[b"BITPIX"])
offset += self.fp.tell() - 80
self.tile = [(decoder_name, (0, 0) + self.size, offset, args)]
def _get_size(
self, headers: dict[bytes, bytes], prefix: bytes
) -> tuple[int, int] | None:
naxis = int(headers[prefix + b"NAXIS"])
if naxis == 0:
return None
if naxis == 1:
return 1, int(headers[prefix + b"NAXIS1"])
else:
return int(headers[prefix + b"NAXIS1"]), int(headers[prefix + b"NAXIS2"])
def _parse_headers(
self, headers: dict[bytes, bytes]
) -> tuple[str, int, tuple[str | int, ...]]:
prefix = b""
decoder_name = "raw"
offset = 0
if (
headers.get(b"XTENSION") == b"'BINTABLE'"
and headers.get(b"ZIMAGE") == b"T"
and headers[b"ZCMPTYPE"] == b"'GZIP_1 '"
):
no_prefix_size = self._get_size(headers, prefix) or (0, 0)
number_of_bits = int(headers[b"BITPIX"])
offset = no_prefix_size[0] * no_prefix_size[1] * (number_of_bits // 8)
prefix = b"Z"
decoder_name = "fits_gzip"
size = self._get_size(headers, prefix)
if not size:
return "", 0, ()
self._size = size
number_of_bits = int(headers[prefix + b"BITPIX"])
if number_of_bits == 8:
self._mode = "L"
elif number_of_bits == 16:
self._mode = "I"
self._mode = "I;16"
elif number_of_bits == 32:
self._mode = "I"
elif number_of_bits in (-32, -64):
self._mode = "F"
offset = math.ceil(self.fp.tell() / 2880) * 2880
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
args = (self.mode, 0, -1) if decoder_name == "raw" else (number_of_bits,)
return decoder_name, offset, args
class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
assert self.fd is not None
value = gzip.decompress(self.fd.read())
rows = []
offset = 0
number_of_bits = min(self.args[0] // 8, 4)
for y in range(self.state.ysize):
row = bytearray()
for x in range(self.state.xsize):
row += value[offset + (4 - number_of_bits) : offset + 4]
offset += 4
rows.append(row)
self.set_as_raw(bytes([pixel for row in rows[::-1] for pixel in row]))
return -1, 0
# --------------------------------------------------------------------
# Registry
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
Image.register_decoder("fits_gzip", FitsGzipDecoder)
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])

View File

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

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