diff --git a/.appveyor.yml b/.appveyor.yml
index 0f5dea9c5..4c5a7f9ee 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -14,7 +14,7 @@ environment:
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64
- ARCHITECTURE: x64
+ ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 32ac6f65e..7244315ac 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index eb27b4bf7..3bb6856f6 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 115c2e9be..cdd51e2bb 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 86cd5b5fa..372f97fd6 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -2,11 +2,12 @@ name: Test Windows
on:
push:
+ branches:
+ - "**"
paths-ignore:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -14,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b7e112f43..05f78704b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
pull_request:
@@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- - ".travis.yml"
- "docs/**"
- "wheels/**"
workflow_dispatch:
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 85d9eba1c..1140aaaad 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -30,7 +30,64 @@ env:
FORCE_COLOR: 1
jobs:
- build:
+ build-1-QEMU-emulated-wheels:
+ name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version:
+ - pp39
+ - pp310
+ - cp38
+ - cp39
+ - cp310
+ - cp311
+ - cp312
+ spec:
+ - manylinux2014
+ - manylinux_2_28
+ - musllinux
+ exclude:
+ - { python-version: pp39, spec: musllinux }
+ - { python-version: pp310, spec: musllinux }
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: true
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+
+ # https://github.com/docker/setup-qemu-action
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Install cibuildwheel
+ run: |
+ python3 -m pip install -r .ci/requirements-cibw.txt
+
+ - name: Build wheels
+ run: |
+ python3 -m cibuildwheel --output-dir wheelhouse
+ env:
+ # Build only the currently selected Linux architecture (so we can
+ # parallelise for speed).
+ CIBW_ARCHS: "aarch64"
+ # Likewise, select only one Python version per job to speed this up.
+ CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
+ # Extra options for manylinux.
+ CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
+ CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
+ path: ./wheelhouse/*.whl
+
+ build-2-native-wheels:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
@@ -62,9 +119,12 @@ jobs:
with:
python-version: "3.x"
- - name: Build wheels
+ - name: Install cibuildwheel
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
+
+ - name: Build wheels
+ run: |
python3 -m cibuildwheel --output-dir wheelhouse
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
@@ -81,18 +141,15 @@ jobs:
path: ./wheelhouse/*.whl
windows:
- name: Windows ${{ matrix.arch }}
+ name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- - arch: x86
- cibw_arch: x86
- - arch: x64
- cibw_arch: AMD64
- - arch: ARM64
- cibw_arch: ARM64
+ - cibw_arch: x86
+ - cibw_arch: AMD64
+ - cibw_arch: ARM64
steps:
- uses: actions/checkout@v4
@@ -106,6 +163,10 @@ jobs:
with:
python-version: "3.x"
+ - name: Install cibuildwheel
+ run: |
+ python.exe -m pip install -r .ci/requirements-cibw.txt
+
- name: Prepare for build
run: |
choco install nasm --no-progress
@@ -114,9 +175,7 @@ jobs:
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
- & python.exe -m pip install -r .ci/requirements-cibw.txt
-
- & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }}
+ & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
shell: pwsh
- name: Build wheels
@@ -157,13 +216,13 @@ jobs:
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
- name: dist-windows-${{ matrix.arch }}
+ name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl
- name: Upload fribidi.dll
uses: actions/upload-artifact@v4
with:
- name: fribidi-windows-${{ matrix.arch }}
+ name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi*
sdist:
@@ -187,7 +246,7 @@ jobs:
pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
- needs: [build, windows, sdist]
+ needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Upload release to PyPI
environment:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 8f8250809..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-if: tag IS present OR type = api
-
-env:
- global:
- - CIBW_ARCHS=aarch64
- - CIBW_SKIP=pp38-*
-
-language: python
-# Default Python version is usually 3.6
-python: "3.12"
-dist: jammy
-services: docker
-
-jobs:
- include:
- - name: "manylinux2014 aarch64"
- os: linux
- arch: arm64
- env:
- - CIBW_BUILD="*manylinux*"
- - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
- - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
- - name: "manylinux_2_28 aarch64"
- os: linux
- arch: arm64
- env:
- - CIBW_BUILD="*manylinux*"
- - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
- - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
- - name: "musllinux aarch64"
- os: linux
- arch: arm64
- env:
- - CIBW_BUILD="*musllinux*"
-
-install:
- - python3 -m pip install -r .ci/requirements-cibw.txt
-
-script:
- - python3 -m cibuildwheel --output-dir wheelhouse
- - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
-
-# Upload wheels to GitHub Releases
-deploy:
- provider: releases
- api_key: $GITHUB_RELEASE_TOKEN
- file_glob: true
- file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
- on:
- repo: python-pillow/Pillow
- tags: true
- skip_cleanup: true
diff --git a/CHANGES.rst b/CHANGES.rst
index 85036f642..62ae2a68b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,27 @@
Changelog (Pillow)
==================
+10.3.0 (unreleased)
+-------------------
+
+- Fix APNG info after seeking backwards more than twice #7701
+ [esoma, radarhere]
+
+- Deprecate ImageCms constants and versions() function #7702
+ [nulano, radarhere]
+
+- Added PerspectiveTransform #7699
+ [radarhere]
+
+- Add support for reading and writing grayscale PFM images #7696
+ [nulano, hugovk]
+
+- Add LCMS2 flags to ImageCms #7676
+ [nulano, radarhere, hugovk]
+
+- Rename x64 to AMD64 in winbuild #7693
+ [nulano]
+
10.2.0 (2024-01-02)
-------------------
diff --git a/README.md b/README.md
index e11bd2faa..6ca870166 100644
--- a/README.md
+++ b/README.md
@@ -48,9 +48,6 @@ As of 2019, Pillow development is
-
@@ -68,10 +65,10 @@ As of 2019, Pillow development is
- ![]()
- ![]()
None:
+ with Image.open("Tests/images/apng/different_durations.png") as im:
+ for i in range(3):
+ im.seek(0)
+ assert im.info["duration"] == 4000
+ im.seek(1)
+ assert im.info["duration"] == 1000
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index c479c384a..8b48e83ad 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -270,7 +270,7 @@ def test_render_scale1():
image1_scale1_compare.load()
assert_image_similar(image1_scale1, image1_scale1_compare, 5)
- # Non-Zero bounding box
+ # Non-zero bounding box
with Image.open(FILE2) as image2_scale1:
image2_scale1.load()
with Image.open(FILE2_COMPARE) as image2_scale1_compare:
@@ -292,7 +292,7 @@ def test_render_scale2():
image1_scale2_compare.load()
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
- # Non-Zero bounding box
+ # Non-zero bounding box
with Image.open(FILE2) as image2_scale2:
image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index bb49a46d3..d8e259b1c 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -6,7 +6,12 @@ import pytest
from PIL import Image, PpmImagePlugin
-from .helper import assert_image_equal_tofile, assert_image_similar, hopper
+from .helper import (
+ assert_image_equal,
+ assert_image_equal_tofile,
+ assert_image_similar,
+ hopper,
+)
# sample ppm stream
TEST_FILE = "Tests/images/hopper.ppm"
@@ -84,20 +89,58 @@ def test_16bit_pgm():
def test_16bit_pgm_write(tmp_path):
with Image.open("Tests/images/16_bit_binary.pgm") as im:
- f = str(tmp_path / "temp.pgm")
- im.save(f, "PPM")
+ filename = str(tmp_path / "temp.pgm")
+ im.save(filename, "PPM")
- assert_image_equal_tofile(im, f)
+ assert_image_equal_tofile(im, filename)
def test_pnm(tmp_path):
with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001)
- f = str(tmp_path / "temp.pnm")
- im.save(f)
+ filename = str(tmp_path / "temp.pnm")
+ im.save(filename)
- assert_image_equal_tofile(im, f)
+ assert_image_equal_tofile(im, filename)
+
+
+def test_pfm(tmp_path):
+ with Image.open("Tests/images/hopper.pfm") as im:
+ assert im.info["scale"] == 1.0
+ assert_image_equal(im, hopper("F"))
+
+ filename = str(tmp_path / "tmp.pfm")
+ im.save(filename)
+
+ assert_image_equal_tofile(im, filename)
+
+
+def test_pfm_big_endian(tmp_path):
+ with Image.open("Tests/images/hopper_be.pfm") as im:
+ assert im.info["scale"] == 2.5
+ assert_image_equal(im, hopper("F"))
+
+ filename = str(tmp_path / "tmp.pfm")
+ im.save(filename)
+
+ assert_image_equal_tofile(im, filename)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ b"Pf 1 1 NaN \0\0\0\0",
+ b"Pf 1 1 inf \0\0\0\0",
+ b"Pf 1 1 -inf \0\0\0\0",
+ b"Pf 1 1 0.0 \0\0\0\0",
+ b"Pf 1 1 -0.0 \0\0\0\0",
+ ],
+)
+def test_pfm_invalid(data):
+ with pytest.raises(ValueError):
+ with Image.open(BytesIO(data)):
+ pass
@pytest.mark.parametrize(
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index b4bf6c8df..5a578dba5 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -403,7 +403,7 @@ class TestCoreResampleCoefficients:
if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0]
- def test_nonzero_coefficients(self):
+ def test_non_zero_coefficients(self):
# regression test for the wrong coefficients calculation
# due to bug https://github.com/python-pillow/Pillow/issues/2161
im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF))
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index 15939ef64..f5d5ab704 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -10,18 +10,25 @@ from .helper import assert_image_equal, assert_image_similar, hopper
class TestImageTransform:
def test_sanity(self):
- im = Image.new("L", (100, 100))
+ im = hopper()
- seq = tuple(range(10))
-
- transform = ImageTransform.AffineTransform(seq[:6])
- im.transform((100, 100), transform)
- transform = ImageTransform.ExtentTransform(seq[:4])
- im.transform((100, 100), transform)
- transform = ImageTransform.QuadTransform(seq[:8])
- im.transform((100, 100), transform)
- transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])])
- im.transform((100, 100), transform)
+ for transform in (
+ ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)),
+ ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)),
+ ImageTransform.ExtentTransform((0, 0) + im.size),
+ ImageTransform.QuadTransform(
+ (0, 0, 0, im.height, im.width, im.height, im.width, 0)
+ ),
+ ImageTransform.MeshTransform(
+ [
+ (
+ (0, 0) + im.size,
+ (0, 0, 0, im.height, im.width, im.height, im.width, 0),
+ )
+ ]
+ ),
+ ):
+ assert_image_equal(im, im.transform(im.size, transform))
def test_info(self):
comment = b"File written by Adobe Photoshop\xa8 4.0"
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 0dde82bd7..810394e6f 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -49,8 +49,8 @@ def skip_missing():
def test_sanity():
# basic smoke test.
# this mostly follows the cms_test outline.
-
- v = ImageCms.versions() # should return four strings
+ with pytest.warns(DeprecationWarning):
+ v = ImageCms.versions() # should return four strings
assert v[0] == "1.0.0 pil"
assert list(map(type, v)) == [str, str, str, str]
@@ -90,6 +90,16 @@ def test_sanity():
hopper().point(t)
+def test_flags():
+ assert ImageCms.Flags.NONE == 0
+ assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
+ assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
+
+ assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16)
+ assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255)
+ assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255)
+
+
def test_name():
skip_missing()
# get profile information for file
@@ -627,3 +637,12 @@ def test_rgb_lab(mode):
im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
+
+
+def test_deprecation() -> None:
+ with pytest.warns(DeprecationWarning):
+ assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")
+ with pytest.warns(DeprecationWarning):
+ assert ImageCms.VERSION == "1.0.0 pil"
+ with pytest.warns(DeprecationWarning):
+ assert isinstance(ImageCms.FLAGS, dict)
diff --git a/docs/PIL.rst b/docs/PIL.rst
index b6944e234..bdbf1373d 100644
--- a/docs/PIL.rst
+++ b/docs/PIL.rst
@@ -77,14 +77,6 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ImageTransform` Module
----------------------------------
-
-.. automodule:: PIL.ImageTransform
- :members:
- :undoc-members:
- :show-inheritance:
-
:mod:`~PIL.PaletteFile` Module
------------------------------
diff --git a/docs/about.rst b/docs/about.rst
index 872ac0ea6..cdb32ca5d 100644
--- a/docs/about.rst
+++ b/docs/about.rst
@@ -6,15 +6,14 @@ Goals
The fork author's goal is to foster and support active development of PIL through:
-- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_
+- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_
- Publicized development activity on `GitHub`_
- Regular releases to the `Python Package Index`_
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
-.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
.. _GitHub: https://github.com/python-pillow/Pillow
-.. _Python Package Index: https://pypi.org/project/Pillow/
+.. _Python Package Index: https://pypi.org/project/pillow/
License
-------
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index a42dc555f..205fcb9ab 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -55,6 +55,43 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
for internal use, so there is no replacement. They can each be replaced
by a single line of code using builtin functions in Python.
+ImageCms constants and versions() function
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.3.0
+
+A number of constants and a function in :py:mod:`.ImageCms` have been deprecated.
+This includes a table of flags based on LittleCMS version 1 which has been
+replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags.
+
+============================================ ====================================================
+Deprecated Use instead
+============================================ ====================================================
+``ImageCms.DESCRIPTION`` No replacement
+``ImageCms.VERSION`` ``PIL.__version__``
+``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION`
+``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT`
+``ImageCms.FLAGS["MATRIXONLY"]`` No replacement
+``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP`
+``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION`
+``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS`
+``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE`
+``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE`
+``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM`
+``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC`
+``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC`
+``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK`
+``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING`
+``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES`
+``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF`
+``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()`
+``ImageCms.versions()`` :py:func:`PIL.features.version_module` with
+ ``feature="littlecms2"``, :py:data:`sys.version` or
+ :py:data:`sys.version_info`, and ``PIL.__version__``
+============================================ ====================================================
+
Removed features
----------------
@@ -118,7 +155,7 @@ Constants
.. versionremoved:: 10.0.0
A number of constants have been removed.
-Instead, ``enum.IntEnum`` classes have been added.
+Instead, :py:class:`enum.IntEnum` classes have been added.
.. note::
@@ -338,8 +375,8 @@ ImageCms.CmsProfile attributes
.. deprecated:: 3.2.0
.. versionremoved:: 8.0.0
-Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
-they issued a :py:exc:`DeprecationWarning`:
+Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed.
+From 6.0.0, they issued a :py:exc:`DeprecationWarning`:
======================== ===================================================
Removed Use instead
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 276838bed..569ccb769 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -696,6 +696,25 @@ PCX
Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.
+PFM
+^^^
+
+.. versionadded:: 10.3.0
+
+Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files
+containing ``F`` data.
+
+Color (PF format) PFM files are not supported.
+
+Opening
+~~~~~~~
+
+The :py:func:`~PIL.Image.open` function sets the following
+:py:attr:`~PIL.Image.Image.info` properties:
+
+**scale**
+ The absolute value of the number stored in the *Scale Factor / Endianness* line.
+
PNG
^^^
diff --git a/docs/index.rst b/docs/index.rst
index 4f577fe9c..558369919 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_ and by direct URL access
-eg. https://pypi.org/project/Pillow/1.0/.
+`_ and by direct URL access
+eg. https://pypi.org/project/pillow/1.0/.
diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst
index 464ab77ea..06965ead3 100644
--- a/docs/reference/ExifTags.rst
+++ b/docs/reference/ExifTags.rst
@@ -4,8 +4,9 @@
:py:mod:`~PIL.ExifTags` Module
==============================
-The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes
-which provide constants and clear-text names for various well-known EXIF tags.
+The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum`
+classes which provide constants and clear-text names for various well-known
+EXIF tags.
.. py:data:: Base
diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst
index 9b9b5e7b2..c4484cbe2 100644
--- a/docs/reference/ImageCms.rst
+++ b/docs/reference/ImageCms.rst
@@ -8,9 +8,34 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management
support using the LittleCMS2 color management engine, based on Kevin
Cazabon's PyCMS library.
+.. autoclass:: ImageCmsProfile
+ :members:
+ :special-members: __init__
.. autoclass:: ImageCmsTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
.. autoexception:: PyCMSError
+Constants
+---------
+
+.. autoclass:: Intent
+ :members:
+ :member-order: bysource
+ :undoc-members:
+ :show-inheritance:
+.. autoclass:: Direction
+ :members:
+ :member-order: bysource
+ :undoc-members:
+ :show-inheritance:
+.. autoclass:: Flags
+ :members:
+ :member-order: bysource
+ :undoc-members:
+ :show-inheritance:
+
Functions
---------
@@ -37,13 +62,15 @@ CmsProfile
----------
The ICC color profiles are wrapped in an instance of the class
-:py:class:`CmsProfile`. The specification ICC.1:2010 contains more
+:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more
information about the meaning of the values in ICC profiles.
For convenience, all XYZ-values are also given as xyY-values (so they
can be easily displayed in a chromaticity diagram, for example).
+.. py:currentmodule:: PIL.ImageCms.core
.. py:class:: CmsProfile
+ :canonical: PIL._imagingcms.CmsProfile
.. py:attribute:: creation_date
:type: Optional[datetime.datetime]
diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst
new file mode 100644
index 000000000..5b0a5ce49
--- /dev/null
+++ b/docs/reference/ImageTransform.rst
@@ -0,0 +1,40 @@
+
+.. py:module:: PIL.ImageTransform
+.. py:currentmodule:: PIL.ImageTransform
+
+:py:mod:`~PIL.ImageTransform` Module
+====================================
+
+The :py:mod:`~PIL.ImageTransform` module contains implementations of
+:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin
+:py:class:`.Image.Transform` methods.
+
+.. autoclass:: Transform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: AffineTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: PerspectiveTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: ExtentTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: QuadTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: MeshTransform
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index 5d6affa94..82c75e373 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -25,6 +25,7 @@ Reference
ImageShow
ImageStat
ImageTk
+ ImageTransform
ImageWin
ExifTags
TiffTags
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index a3f238119..705ca0415 100644
--- a/docs/releasenotes/10.0.0.rst
+++ b/docs/releasenotes/10.0.0.rst
@@ -43,7 +43,7 @@ Constants
^^^^^^^^^
A number of constants have been removed.
-Instead, ``enum.IntEnum`` classes have been added.
+Instead, :py:class:`enum.IntEnum` classes have been added.
===================================================== ============================================================
Removed Use instead
diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst
new file mode 100644
index 000000000..8772a382d
--- /dev/null
+++ b/docs/releasenotes/10.3.0.rst
@@ -0,0 +1,81 @@
+10.3.0
+------
+
+Backwards Incompatible Changes
+==============================
+
+TODO
+^^^^
+
+Deprecations
+============
+
+ImageCms constants and versions() function
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A number of constants and a function in :py:mod:`.ImageCms` have been deprecated.
+This includes a table of flags based on LittleCMS version 1 which has been replaced
+with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags.
+
+============================================ ====================================================
+Deprecated Use instead
+============================================ ====================================================
+``ImageCms.DESCRIPTION`` No replacement
+``ImageCms.VERSION`` ``PIL.__version__``
+``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION`
+``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT`
+``ImageCms.FLAGS["MATRIXONLY"]`` No replacement
+``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP`
+``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION`
+``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS`
+``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE`
+``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE`
+``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM`
+``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC`
+``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC`
+``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK`
+``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
+``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING`
+``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES`
+``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF`
+``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()`
+``ImageCms.versions()`` :py:func:`PIL.features.version_module` with
+ ``feature="littlecms2"``, :py:data:`sys.version` or
+ :py:data:`sys.version_info`, and ``PIL.__version__``
+============================================ ====================================================
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+Added PerspectiveTransform
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning
+that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding
+subclass of :py:class:`~PIL.ImageTransform.Transform`.
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+Portable FloatMap (PFM) images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added for reading and writing grayscale (Pf format)
+Portable FloatMap (PFM) files containing ``F`` data.
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index 2bf299dd3..1fc245c9a 100644
--- a/docs/releasenotes/8.0.0.rst
+++ b/docs/releasenotes/8.0.0.rst
@@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring
ImageCms.CmsProfile attributes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed:
+Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed:
======================== ===================================================
Removed Use instead
diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst
index 02da702a7..6400218f4 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -51,7 +51,7 @@ Constants
^^^^^^^^^
A number of constants have been deprecated and will be removed in Pillow 10.0.0
-(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
+(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added.
.. note::
diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst
index fde2faae3..16075ce95 100644
--- a/docs/releasenotes/9.3.0.rst
+++ b/docs/releasenotes/9.3.0.rst
@@ -33,8 +33,9 @@ Added ExifTags enums
^^^^^^^^^^^^^^^^^^^^
The data from :py:data:`~PIL.ExifTags.TAGS` and
-:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum``
-classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`.
+:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as
+:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and
+:py:data:`~PIL.ExifTags.GPS`.
Security
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index d8034853c..e86f8082b 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 10.3.0
10.2.0
10.1.0
10.0.1
diff --git a/pyproject.toml b/pyproject.toml
index da2537b21..789df6f5e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -146,10 +146,7 @@ exclude = [
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
- '^src/PIL/ImageMath.py$',
- '^src/PIL/ImageMorph.py$',
'^src/PIL/ImageQt.py$',
- '^src/PIL/ImageShow.py$',
'^src/PIL/ImImagePlugin.py$',
'^src/PIL/MicImagePlugin.py$',
'^src/PIL/PdfParser.py$',
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index 7dce2d60f..e69890bab 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -15,7 +15,7 @@ import math
from . import Image, ImageFile
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:6] == b"SIMPLE"
@@ -23,8 +23,10 @@ class FitsImageFile(ImageFile.ImageFile):
format = "FITS"
format_description = "FITS"
- def _open(self):
- headers = {}
+ def _open(self) -> None:
+ assert self.fp is not None
+
+ headers: dict[bytes, bytes] = {}
while True:
header = self.fp.read(80)
if not header:
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index d84876eb6..7bb4736af 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -27,6 +27,8 @@
"""
from __future__ import annotations
+from io import BytesIO
+
from . import ImageFile, ImagePalette, UnidentifiedImageError
from ._binary import i16be as i16
from ._binary import i32be as i32
@@ -43,8 +45,10 @@ class GdImageFile(ImageFile.ImageFile):
format = "GD"
format_description = "GD uncompressed images"
- def _open(self):
+ def _open(self) -> None:
# Header
+ assert self.fp is not None
+
s = self.fp.read(1037)
if i16(s) not in [65534, 65535]:
@@ -76,7 +80,7 @@ class GdImageFile(ImageFile.ImageFile):
]
-def open(fp, mode="r"):
+def open(fp: BytesIO, mode: str = "r") -> GdImageFile:
"""
Load texture from a GD image file.
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 1bba9aad2..553f36703 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -242,7 +242,7 @@ MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "
_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B")
-def getmodebase(mode):
+def getmodebase(mode: str) -> str:
"""
Gets the "base" mode for given mode. This function returns "L" for
images that contain grayscale data, and "RGB" for images that
@@ -282,7 +282,7 @@ def getmodebandnames(mode):
return ImageMode.getmode(mode).bands
-def getmodebands(mode):
+def getmodebands(mode: str) -> int:
"""
Gets the number of individual bands for this mode.
@@ -583,7 +583,9 @@ class Image:
else:
self.load()
- def _dump(self, file=None, format=None, **options):
+ def _dump(
+ self, file: str | None = None, format: str | None = None, **options
+ ) -> str:
suffix = ""
if format:
suffix = "." + format
@@ -708,7 +710,7 @@ class Image:
self.putpalette(palette)
self.frombytes(data)
- def tobytes(self, encoder_name="raw", *args):
+ def tobytes(self, encoder_name: str = "raw", *args) -> bytes:
"""
Return image as a bytes object.
@@ -786,7 +788,7 @@ class Image:
]
)
- def frombytes(self, data, decoder_name="raw", *args):
+ def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None:
"""
Loads this image with pixel data from a bytes object.
@@ -873,7 +875,7 @@ class Image:
def convert(
self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256
- ):
+ ) -> Image:
"""
Returns a converted copy of this image. For the "P" mode, this
method translates pixels through the palette. If mode is
@@ -1295,7 +1297,7 @@ class Image:
]
return merge(self.mode, ims)
- def getbands(self):
+ def getbands(self) -> tuple[str, ...]:
"""
Returns a tuple containing the name of each band in this image.
For example, ``getbands`` on an RGB image returns ("R", "G", "B").
@@ -1305,7 +1307,7 @@ class Image:
"""
return ImageMode.getmode(self.mode).bands
- def getbbox(self, *, alpha_only=True):
+ def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]:
"""
Calculates the bounding box of the non-zero regions in the
image.
@@ -2493,7 +2495,7 @@ class Image:
_show(self, title=title)
- def split(self):
+ def split(self) -> tuple[Image, ...]:
"""
Split this image into individual bands. This method returns a
tuple of individual image bands from an image. For example,
@@ -2666,6 +2668,10 @@ class Image:
def transform(self, size, data, resample, fill=1):
# Return result
+ Implementations of :py:class:`~PIL.Image.ImageTransformHandler`
+ for some of the :py:class:`Transform` methods are provided
+ in :py:mod:`~PIL.ImageTransform`.
+
It may also be an object with a ``method.getdata`` method
that returns a tuple supplying new ``method`` and ``data`` values::
@@ -3431,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None:
OPEN[id] = factory, accept
-def register_mime(id, mimetype):
+def register_mime(id: str, mimetype: str) -> None:
"""
Registers an image MIME type by populating ``Image.MIME``. This function
should not be used in application code.
@@ -3446,7 +3452,7 @@ def register_mime(id, mimetype):
MIME[id.upper()] = mimetype
-def register_save(id, driver):
+def register_save(id: str, driver) -> None:
"""
Registers an image save function. This function should not be
used in application code.
@@ -3480,7 +3486,7 @@ def register_extension(id, extension) -> None:
EXTENSION[extension.lower()] = id.upper()
-def register_extensions(id, extensions):
+def register_extensions(id, extensions) -> None:
"""
Registers image extensions. This function should not be
used in application code.
@@ -3501,7 +3507,7 @@ def registered_extensions():
return EXTENSION
-def register_decoder(name, decoder):
+def register_decoder(name: str, decoder) -> None:
"""
Registers an image decoder. This function should not be
used in application code.
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 643fce830..3e40105e4 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -4,6 +4,9 @@
# Optional color management support, based on Kevin Cazabon's PyCMS
# library.
+# Originally released under LGPL. Graciously donated to PIL in
+# March 2009, for distribution under the standard PIL license
+
# History:
# 2009-03-08 fl Added to PIL.
@@ -16,10 +19,14 @@
# below for the original description.
from __future__ import annotations
+import operator
import sys
-from enum import IntEnum
+from enum import IntEnum, IntFlag
+from functools import reduce
+from typing import Any
from . import Image
+from ._deprecate import deprecate
try:
from . import _imagingcms
@@ -30,7 +37,7 @@ except ImportError as ex:
_imagingcms = DeferredError.new(ex)
-DESCRIPTION = """
+_DESCRIPTION = """
pyCMS
a Python / PIL interface to the littleCMS ICC Color Management System
@@ -93,7 +100,22 @@ pyCMS
"""
-VERSION = "1.0.0 pil"
+_VERSION = "1.0.0 pil"
+
+
+def __getattr__(name: str) -> Any:
+ if name == "DESCRIPTION":
+ deprecate("PIL.ImageCms.DESCRIPTION", 12)
+ return _DESCRIPTION
+ elif name == "VERSION":
+ deprecate("PIL.ImageCms.VERSION", 12)
+ return _VERSION
+ elif name == "FLAGS":
+ deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags")
+ return _FLAGS
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
+
# --------------------------------------------------------------------.
@@ -119,7 +141,70 @@ class Direction(IntEnum):
#
# flags
-FLAGS = {
+
+class Flags(IntFlag):
+ """Flags and documentation are taken from ``lcms2.h``."""
+
+ NONE = 0
+ NOCACHE = 0x0040
+ """Inhibit 1-pixel cache"""
+ NOOPTIMIZE = 0x0100
+ """Inhibit optimizations"""
+ NULLTRANSFORM = 0x0200
+ """Don't transform anyway"""
+ GAMUTCHECK = 0x1000
+ """Out of Gamut alarm"""
+ SOFTPROOFING = 0x4000
+ """Do softproofing"""
+ BLACKPOINTCOMPENSATION = 0x2000
+ NOWHITEONWHITEFIXUP = 0x0004
+ """Don't fix scum dot"""
+ HIGHRESPRECALC = 0x0400
+ """Use more memory to give better accuracy"""
+ LOWRESPRECALC = 0x0800
+ """Use less memory to minimize resources"""
+ # this should be 8BITS_DEVICELINK, but that is not a valid name in Python:
+ USE_8BITS_DEVICELINK = 0x0008
+ """Create 8 bits devicelinks"""
+ GUESSDEVICECLASS = 0x0020
+ """Guess device class (for ``transform2devicelink``)"""
+ KEEP_SEQUENCE = 0x0080
+ """Keep profile sequence for devicelink creation"""
+ FORCE_CLUT = 0x0002
+ """Force CLUT optimization"""
+ CLUT_POST_LINEARIZATION = 0x0001
+ """create postlinearization tables if possible"""
+ CLUT_PRE_LINEARIZATION = 0x0010
+ """create prelinearization tables if possible"""
+ NONEGATIVES = 0x8000
+ """Prevent negative numbers in floating point transforms"""
+ COPY_ALPHA = 0x04000000
+ """Alpha channels are copied on ``cmsDoTransform()``"""
+ NODEFAULTRESOURCEDEF = 0x01000000
+
+ _GRIDPOINTS_1 = 1 << 16
+ _GRIDPOINTS_2 = 2 << 16
+ _GRIDPOINTS_4 = 4 << 16
+ _GRIDPOINTS_8 = 8 << 16
+ _GRIDPOINTS_16 = 16 << 16
+ _GRIDPOINTS_32 = 32 << 16
+ _GRIDPOINTS_64 = 64 << 16
+ _GRIDPOINTS_128 = 128 << 16
+
+ @staticmethod
+ def GRIDPOINTS(n: int) -> Flags:
+ """
+ Fine-tune control over number of gridpoints
+
+ :param n: :py:class:`int` in range ``0 <= n <= 255``
+ """
+ return Flags.NONE | ((n & 0xFF) << 16)
+
+
+_MAX_FLAG = reduce(operator.or_, Flags)
+
+
+_FLAGS = {
"MATRIXINPUT": 1,
"MATRIXOUTPUT": 2,
"MATRIXONLY": (1 | 2),
@@ -142,11 +227,6 @@ FLAGS = {
"GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints
}
-_MAX_FLAG = 0
-for flag in FLAGS.values():
- if isinstance(flag, int):
- _MAX_FLAG = _MAX_FLAG | flag
-
# --------------------------------------------------------------------.
# Experimental PIL-level API
@@ -218,7 +298,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
intent=Intent.PERCEPTUAL,
proof=None,
proof_intent=Intent.ABSOLUTE_COLORIMETRIC,
- flags=0,
+ flags=Flags.NONE,
):
if proof is None:
self.transform = core.buildTransform(
@@ -303,7 +383,7 @@ def profileToProfile(
renderingIntent=Intent.PERCEPTUAL,
outputMode=None,
inPlace=False,
- flags=0,
+ flags=Flags.NONE,
):
"""
(pyCMS) Applies an ICC transformation to a given image, mapping from
@@ -420,7 +500,7 @@ def buildTransform(
inMode,
outMode,
renderingIntent=Intent.PERCEPTUAL,
- flags=0,
+ flags=Flags.NONE,
):
"""
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
@@ -482,7 +562,7 @@ def buildTransform(
raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
raise PyCMSError(msg)
try:
@@ -505,7 +585,7 @@ def buildProofTransform(
outMode,
renderingIntent=Intent.PERCEPTUAL,
proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC,
- flags=FLAGS["SOFTPROOFING"],
+ flags=Flags.SOFTPROOFING,
):
"""
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
@@ -586,7 +666,7 @@ def buildProofTransform(
raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
raise PyCMSError(msg)
try:
@@ -1004,4 +1084,9 @@ def versions():
(pyCMS) Fetches versions.
"""
- return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__
+ deprecate(
+ "PIL.ImageCms.versions()",
+ 12,
+ '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)',
+ )
+ return _VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 0923979af..5ba5a6f82 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -514,7 +514,7 @@ class Parser:
# --------------------------------------------------------------------
-def _save(im, fp, tile, bufsize=0):
+def _save(im, fp, tile, bufsize=0) -> None:
"""Helper to save image based on tile list
:param im: Image object.
@@ -616,6 +616,8 @@ class PyCodecState:
class PyCodec:
+ fd: io.BytesIO | None
+
def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
@@ -713,7 +715,7 @@ class PyDecoder(PyCodec):
msg = "unavailable in base decoder"
raise NotImplementedError(msg)
- def set_as_raw(self, data, rawmode=None):
+ def set_as_raw(self, data: bytes, rawmode=None) -> None:
"""
Convenience method to set the internal image from a stream of raw data
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 8213d030a..a63b73b33 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -584,22 +584,13 @@ class FreeTypeFont:
_string_length_check(text)
if start is None:
start = (0, 0)
- im = None
- size = None
def fill(width, height):
- nonlocal im, size
-
size = (width, height)
- if Image.MAX_IMAGE_PIXELS is not None:
- pixels = max(1, width) * max(1, height)
- if pixels > 2 * Image.MAX_IMAGE_PIXELS:
- return
+ Image._decompression_bomb_check(size)
+ return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
- im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
- return im
-
- offset = self.font.render(
+ return self.font.render(
text,
fill,
mode,
@@ -612,8 +603,6 @@ class FreeTypeFont:
start[0],
start[1],
)
- Image._decompression_bomb_check(size)
- return im, offset
def font_variant(
self, font=None, size=None, index=None, encoding=None, layout_engine=None
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index b77f4bce5..a7652f237 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -17,6 +17,8 @@
from __future__ import annotations
import builtins
+from types import CodeType
+from typing import Any
from . import Image, _imagingmath
@@ -24,10 +26,10 @@ from . import Image, _imagingmath
class _Operand:
"""Wraps an image operand, providing standard operators"""
- def __init__(self, im):
+ def __init__(self, im: Image.Image):
self.im = im
- def __fixup(self, im1):
+ def __fixup(self, im1: _Operand | float) -> Image.Image:
# convert image to suitable mode
if isinstance(im1, _Operand):
# argument was an image.
@@ -45,122 +47,131 @@ class _Operand:
else:
return Image.new("F", self.im.size, im1)
- def apply(self, op, im1, im2=None, mode=None):
- im1 = self.__fixup(im1)
+ def apply(
+ self,
+ op: str,
+ im1: _Operand | float,
+ im2: _Operand | float | None = None,
+ mode: str | None = None,
+ ) -> _Operand:
+ im_1 = self.__fixup(im1)
if im2 is None:
# unary operation
- out = Image.new(mode or im1.mode, im1.size, None)
- im1.load()
+ out = Image.new(mode or im_1.mode, im_1.size, None)
+ im_1.load()
try:
- op = getattr(_imagingmath, op + "_" + im1.mode)
+ op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e
- _imagingmath.unop(op, out.im.id, im1.im.id)
+ _imagingmath.unop(op, out.im.id, im_1.im.id)
else:
# binary operation
- im2 = self.__fixup(im2)
- if im1.mode != im2.mode:
+ im_2 = self.__fixup(im2)
+ if im_1.mode != im_2.mode:
# convert both arguments to floating point
- if im1.mode != "F":
- im1 = im1.convert("F")
- if im2.mode != "F":
- im2 = im2.convert("F")
- if im1.size != im2.size:
+ if im_1.mode != "F":
+ im_1 = im_1.convert("F")
+ if im_2.mode != "F":
+ im_2 = im_2.convert("F")
+ if im_1.size != im_2.size:
# crop both arguments to a common size
- size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1]))
- if im1.size != size:
- im1 = im1.crop((0, 0) + size)
- if im2.size != size:
- im2 = im2.crop((0, 0) + size)
- out = Image.new(mode or im1.mode, im1.size, None)
- im1.load()
- im2.load()
+ size = (
+ min(im_1.size[0], im_2.size[0]),
+ min(im_1.size[1], im_2.size[1]),
+ )
+ if im_1.size != size:
+ im_1 = im_1.crop((0, 0) + size)
+ if im_2.size != size:
+ im_2 = im_2.crop((0, 0) + size)
+ out = Image.new(mode or im_1.mode, im_1.size, None)
+ im_1.load()
+ im_2.load()
try:
- op = getattr(_imagingmath, op + "_" + im1.mode)
+ op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e
- _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id)
+ _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id)
return _Operand(out)
# unary operators
- def __bool__(self):
+ def __bool__(self) -> bool:
# an image is "true" if it contains at least one non-zero pixel
return self.im.getbbox() is not None
- def __abs__(self):
+ def __abs__(self) -> _Operand:
return self.apply("abs", self)
- def __pos__(self):
+ def __pos__(self) -> _Operand:
return self
- def __neg__(self):
+ def __neg__(self) -> _Operand:
return self.apply("neg", self)
# binary operators
- def __add__(self, other):
+ def __add__(self, other: _Operand | float) -> _Operand:
return self.apply("add", self, other)
- def __radd__(self, other):
+ def __radd__(self, other: _Operand | float) -> _Operand:
return self.apply("add", other, self)
- def __sub__(self, other):
+ def __sub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", self, other)
- def __rsub__(self, other):
+ def __rsub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", other, self)
- def __mul__(self, other):
+ def __mul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", self, other)
- def __rmul__(self, other):
+ def __rmul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", other, self)
- def __truediv__(self, other):
+ def __truediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", self, other)
- def __rtruediv__(self, other):
+ def __rtruediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", other, self)
- def __mod__(self, other):
+ def __mod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", self, other)
- def __rmod__(self, other):
+ def __rmod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", other, self)
- def __pow__(self, other):
+ def __pow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", self, other)
- def __rpow__(self, other):
+ def __rpow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", other, self)
# bitwise
- def __invert__(self):
+ def __invert__(self) -> _Operand:
return self.apply("invert", self)
- def __and__(self, other):
+ def __and__(self, other: _Operand | float) -> _Operand:
return self.apply("and", self, other)
- def __rand__(self, other):
+ def __rand__(self, other: _Operand | float) -> _Operand:
return self.apply("and", other, self)
- def __or__(self, other):
+ def __or__(self, other: _Operand | float) -> _Operand:
return self.apply("or", self, other)
- def __ror__(self, other):
+ def __ror__(self, other: _Operand | float) -> _Operand:
return self.apply("or", other, self)
- def __xor__(self, other):
+ def __xor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", self, other)
- def __rxor__(self, other):
+ def __rxor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", other, self)
- def __lshift__(self, other):
+ def __lshift__(self, other: _Operand | float) -> _Operand:
return self.apply("lshift", self, other)
- def __rshift__(self, other):
+ def __rshift__(self, other: _Operand | float) -> _Operand:
return self.apply("rshift", self, other)
# logical
@@ -170,56 +181,61 @@ class _Operand:
def __ne__(self, other):
return self.apply("ne", self, other)
- def __lt__(self, other):
+ def __lt__(self, other: _Operand | float) -> _Operand:
return self.apply("lt", self, other)
- def __le__(self, other):
+ def __le__(self, other: _Operand | float) -> _Operand:
return self.apply("le", self, other)
- def __gt__(self, other):
+ def __gt__(self, other: _Operand | float) -> _Operand:
return self.apply("gt", self, other)
- def __ge__(self, other):
+ def __ge__(self, other: _Operand | float) -> _Operand:
return self.apply("ge", self, other)
# conversions
-def imagemath_int(self):
+def imagemath_int(self: _Operand) -> _Operand:
return _Operand(self.im.convert("I"))
-def imagemath_float(self):
+def imagemath_float(self: _Operand) -> _Operand:
return _Operand(self.im.convert("F"))
# logical
-def imagemath_equal(self, other):
+def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("eq", self, other, mode="I")
-def imagemath_notequal(self, other):
+def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("ne", self, other, mode="I")
-def imagemath_min(self, other):
+def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("min", self, other)
-def imagemath_max(self, other):
+def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("max", self, other)
-def imagemath_convert(self, mode):
+def imagemath_convert(self: _Operand, mode: str) -> _Operand:
return _Operand(self.im.convert(mode))
-ops = {}
-for k, v in list(globals().items()):
- if k[:10] == "imagemath_":
- ops[k[10:]] = v
+ops = {
+ "int": imagemath_int,
+ "float": imagemath_float,
+ "equal": imagemath_equal,
+ "notequal": imagemath_notequal,
+ "min": imagemath_min,
+ "max": imagemath_max,
+ "convert": imagemath_convert,
+}
-def eval(expression, _dict={}, **kw):
+def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any:
"""
Evaluates an image expression.
@@ -233,7 +249,7 @@ def eval(expression, _dict={}, **kw):
"""
# build execution namespace
- args = ops.copy()
+ args: dict[str, Any] = ops.copy()
for k in list(_dict.keys()) + list(kw.keys()):
if "__" in k or hasattr(builtins, k):
msg = f"'{k}' not allowed"
@@ -247,7 +263,7 @@ def eval(expression, _dict={}, **kw):
compiled_code = compile(expression, "", "eval")
- def scan(code):
+ def scan(code: CodeType) -> None:
for const in code.co_consts:
if type(const) is type(compiled_code):
scan(const)
diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py
index 282e7d2a5..534c6291a 100644
--- a/src/PIL/ImageMorph.py
+++ b/src/PIL/ImageMorph.py
@@ -62,12 +62,14 @@ class LutBuilder:
"""
- def __init__(self, patterns=None, op_name=None):
+ def __init__(
+ self, patterns: list[str] | None = None, op_name: str | None = None
+ ) -> None:
if patterns is not None:
self.patterns = patterns
else:
self.patterns = []
- self.lut = None
+ self.lut: bytearray | None = None
if op_name is not None:
known_patterns = {
"corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"],
@@ -87,25 +89,27 @@ class LutBuilder:
self.patterns = known_patterns[op_name]
- def add_patterns(self, patterns):
+ def add_patterns(self, patterns: list[str]) -> None:
self.patterns += patterns
- def build_default_lut(self):
+ def build_default_lut(self) -> None:
symbols = [0, 1]
m = 1 << 4 # pos of current pixel
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
- def get_lut(self):
+ def get_lut(self) -> bytearray | None:
return self.lut
- def _string_permute(self, pattern, permutation):
+ def _string_permute(self, pattern: str, permutation: list[int]) -> str:
"""string_permute takes a pattern and a permutation and returns the
string permuted according to the permutation list.
"""
assert len(permutation) == 9
return "".join(pattern[p] for p in permutation)
- def _pattern_permute(self, basic_pattern, options, basic_result):
+ def _pattern_permute(
+ self, basic_pattern: str, options: str, basic_result: int
+ ) -> list[tuple[str, int]]:
"""pattern_permute takes a basic pattern and its result and clones
the pattern according to the modifications described in the $options
parameter. It returns a list of all cloned patterns."""
@@ -135,12 +139,13 @@ class LutBuilder:
return patterns
- def build_lut(self):
+ def build_lut(self) -> bytearray:
"""Compile all patterns into a morphology lut.
TBD :Build based on (file) morphlut:modify_lut
"""
self.build_default_lut()
+ assert self.lut is not None
patterns = []
# Parse and create symmetries of the patterns strings
@@ -159,10 +164,10 @@ class LutBuilder:
patterns += self._pattern_permute(pattern, options, result)
# compile the patterns into regular expressions for speed
- for i, pattern in enumerate(patterns):
+ compiled_patterns = []
+ for pattern in patterns:
p = pattern[0].replace(".", "X").replace("X", "[01]")
- p = re.compile(p)
- patterns[i] = (p, pattern[1])
+ compiled_patterns.append((re.compile(p), pattern[1]))
# Step through table and find patterns that match.
# Note that all the patterns are searched. The last one
@@ -172,8 +177,8 @@ class LutBuilder:
bitpattern = bin(i)[2:]
bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1]
- for p, r in patterns:
- if p.match(bitpattern):
+ for pattern, r in compiled_patterns:
+ if pattern.match(bitpattern):
self.lut[i] = [0, 1][r]
return self.lut
@@ -182,7 +187,12 @@ class LutBuilder:
class MorphOp:
"""A class for binary morphological operators"""
- def __init__(self, lut=None, op_name=None, patterns=None):
+ def __init__(
+ self,
+ lut: bytearray | None = None,
+ op_name: str | None = None,
+ patterns: list[str] | None = None,
+ ) -> None:
"""Create a binary morphological operator"""
self.lut = lut
if op_name is not None:
@@ -190,7 +200,7 @@ class MorphOp:
elif patterns is not None:
self.lut = LutBuilder(patterns=patterns).build_lut()
- def apply(self, image):
+ def apply(self, image: Image.Image):
"""Run a single morphological operation on an image
Returns a tuple of the number of changed pixels and the
@@ -206,7 +216,7 @@ class MorphOp:
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
- def match(self, image):
+ def match(self, image: Image.Image):
"""Get a list of coordinates matching the morphological operation on
an image.
@@ -221,7 +231,7 @@ class MorphOp:
raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id)
- def get_on_pixels(self, image):
+ def get_on_pixels(self, image: Image.Image):
"""Get a list of all turned on pixels in a binary image
Returns a list of tuples of (x,y) coordinates
@@ -232,7 +242,7 @@ class MorphOp:
raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.im.id)
- def load_lut(self, filename):
+ def load_lut(self, filename: str) -> None:
"""Load an operator from an mrl file"""
with open(filename, "rb") as f:
self.lut = bytearray(f.read())
@@ -242,7 +252,7 @@ class MorphOp:
msg = "Wrong size operator file!"
raise Exception(msg)
- def save_lut(self, filename):
+ def save_lut(self, filename: str) -> None:
"""Save an operator to an mrl file"""
if self.lut is None:
msg = "No operator loaded"
@@ -250,6 +260,6 @@ class MorphOp:
with open(filename, "wb") as f:
f.write(self.lut)
- def set_lut(self, lut):
+ def set_lut(self, lut: bytearray | None) -> None:
"""Set the lut from an external source"""
self.lut = lut
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index fbcfa309d..2b6cecc61 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -192,7 +192,7 @@ class ImagePalette:
# Internal
-def raw(rawmode, data):
+def raw(rawmode, data) -> ImagePalette:
palette = ImagePalette()
palette.rawmode = rawmode
palette.palette = data
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index fad3e0980..c03122c11 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -13,18 +13,20 @@
#
from __future__ import annotations
+import abc
import os
import shutil
import subprocess
import sys
from shlex import quote
+from typing import Any
from . import Image
_viewers = []
-def register(viewer, order=1):
+def register(viewer, order: int = 1) -> None:
"""
The :py:func:`register` function is used to register additional viewers::
@@ -49,7 +51,7 @@ def register(viewer, order=1):
_viewers.insert(0, viewer)
-def show(image, title=None, **options):
+def show(image: Image.Image, title: str | None = None, **options: Any) -> bool:
r"""
Display a given image.
@@ -69,7 +71,7 @@ class Viewer:
# main api
- def show(self, image, **options):
+ def show(self, image: Image.Image, **options: Any) -> int:
"""
The main function for displaying an image.
Converts the given image to the target format and displays it.
@@ -87,16 +89,16 @@ class Viewer:
# hook methods
- format = None
+ format: str | None = None
"""The format to convert the image into."""
- options = {}
+ options: dict[str, Any] = {}
"""Additional options used to convert the image."""
- def get_format(self, image):
+ def get_format(self, image: Image.Image) -> str | None:
"""Return format name, or ``None`` to save as PGM/PPM."""
return self.format
- def get_command(self, file, **options):
+ def get_command(self, file: str, **options: Any) -> str:
"""
Returns the command used to display the file.
Not implemented in the base class.
@@ -104,15 +106,15 @@ class Viewer:
msg = "unavailable in base viewer"
raise NotImplementedError(msg)
- def save_image(self, image):
+ def save_image(self, image: Image.Image) -> str:
"""Save to temporary file and return filename."""
return image._dump(format=self.get_format(image), **self.options)
- def show_image(self, image, **options):
+ def show_image(self, image: Image.Image, **options: Any) -> int:
"""Display the given image."""
return self.show_file(self.save_image(image), **options)
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -129,7 +131,7 @@ class WindowsViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
- def get_command(self, file, **options):
+ def get_command(self, file: str, **options: Any) -> str:
return (
f'start "Pillow" /WAIT "{file}" '
"&& ping -n 4 127.0.0.1 >NUL "
@@ -147,14 +149,14 @@ class MacViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
- def get_command(self, file, **options):
+ def get_command(self, file: str, **options: Any) -> str:
# on darwin open returns immediately resulting in the temp
# file removal while app is opening
command = "open -a Preview.app"
command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
return command
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -180,7 +182,11 @@ class UnixViewer(Viewer):
format = "PNG"
options = {"compress_level": 1, "save_all": True}
- def get_command(self, file, **options):
+ @abc.abstractmethod
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
+ pass # pragma: no cover
+
+ def get_command(self, file: str, **options: Any) -> str:
command = self.get_command_ex(file, **options)[0]
return f"({command} {quote(file)}"
@@ -190,11 +196,11 @@ class XDGViewer(UnixViewer):
The freedesktop.org ``xdg-open`` command.
"""
- def get_command_ex(self, file, **options):
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
command = executable = "xdg-open"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -208,13 +214,15 @@ class DisplayViewer(UnixViewer):
This viewer supports the ``title`` parameter.
"""
- def get_command_ex(self, file, title=None, **options):
+ def get_command_ex(
+ self, file: str, title: str | None = None, **options: Any
+ ) -> tuple[str, str]:
command = executable = "display"
if title:
command += f" -title {quote(title)}"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -231,12 +239,12 @@ class DisplayViewer(UnixViewer):
class GmDisplayViewer(UnixViewer):
"""The GraphicsMagick ``gm display`` command."""
- def get_command_ex(self, file, **options):
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
executable = "gm"
command = "gm display"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -247,12 +255,12 @@ class GmDisplayViewer(UnixViewer):
class EogViewer(UnixViewer):
"""The GNOME Image Viewer ``eog`` command."""
- def get_command_ex(self, file, **options):
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
executable = "eog"
command = "eog -n"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -266,7 +274,9 @@ class XVViewer(UnixViewer):
This viewer supports the ``title`` parameter.
"""
- def get_command_ex(self, file, title=None, **options):
+ def get_command_ex(
+ self, file: str, title: str | None = None, **options: Any
+ ) -> tuple[str, str]:
# note: xv is pretty outdated. most modern systems have
# imagemagick's display command instead.
command = executable = "xv"
@@ -274,7 +284,7 @@ class XVViewer(UnixViewer):
command += f" -name {quote(title)}"
return command, executable
- def show_file(self, path, **options):
+ def show_file(self, path: str, **options: Any) -> int:
"""
Display given file.
"""
@@ -304,7 +314,7 @@ if sys.platform not in ("win32", "darwin"): # unixoids
class IPythonViewer(Viewer):
"""The viewer for IPython frontends."""
- def show_image(self, image, **options):
+ def show_image(self, image: Image.Image, **options: Any) -> int:
ipython_display(image)
return 1
diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py
index 84c81f184..6aa82dadd 100644
--- a/src/PIL/ImageTransform.py
+++ b/src/PIL/ImageTransform.py
@@ -20,12 +20,14 @@ from . import Image
class Transform(Image.ImageTransformHandler):
+ """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`."""
+
method: Image.Transform
def __init__(self, data: Sequence[int]) -> None:
self.data = data
- def getdata(self) -> tuple[int, Sequence[int]]:
+ def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
return self.method, self.data
def transform(
@@ -34,6 +36,7 @@ class Transform(Image.ImageTransformHandler):
image: Image.Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]],
) -> Image.Image:
+ """Perform the transform. Called from :py:meth:`.Image.transform`."""
# can be overridden
method, data = self.getdata()
return image.transform(size, method, data, **options)
@@ -51,7 +54,7 @@ class AffineTransform(Transform):
This function can be used to scale, translate, rotate, and shear the
original image.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
from an affine transform matrix.
@@ -60,6 +63,26 @@ class AffineTransform(Transform):
method = Image.Transform.AFFINE
+class PerspectiveTransform(Transform):
+ """
+ Define a perspective image transform.
+
+ This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel
+ (x, y) in the output image, the new value is taken from a position
+ ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in
+ the input image, rounded to nearest pixel.
+
+ This function can be used to scale, translate, rotate, and shear the
+ original image.
+
+ See :py:meth:`.Image.transform`
+
+ :param matrix: An 8-tuple (a, b, c, d, e, f, g, h).
+ """
+
+ method = Image.Transform.PERSPECTIVE
+
+
class ExtentTransform(Transform):
"""
Define a transform to extract a subregion from an image.
@@ -73,7 +96,7 @@ class ExtentTransform(Transform):
rectangle in the current image. It is slightly slower than crop, but about
as fast as a corresponding resize operation.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the
input image's coordinate system. See :ref:`coordinate-system`.
@@ -89,7 +112,7 @@ class QuadTransform(Transform):
Maps a quadrilateral (a region defined by four corners) from the image to a
rectangle of the given size.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the
upper left, lower left, lower right, and upper right corner of the
@@ -104,7 +127,7 @@ class MeshTransform(Transform):
Define a mesh image transform. A mesh transform consists of one or more
individual quad transforms.
- See :py:meth:`~PIL.Image.Image.transform`
+ See :py:meth:`.Image.transform`
:param data: A list of (bbox, quad) tuples.
"""
diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index 7469c592d..abb3fb762 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -33,10 +33,12 @@ class ImtImageFile(ImageFile.ImageFile):
format = "IMT"
format_description = "IM Tools"
- def _open(self):
+ def _open(self) -> None:
# Quick rejection: if there's not a LF among the first
# 100 bytes, this is (probably) not a text header.
+ assert self.fp is not None
+
buffer = self.fp.read(100)
if b"\n" not in buffer:
msg = "not an IM file"
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index 9a85c0d15..27972236c 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -22,8 +22,8 @@ import struct
from . import Image, ImageFile
-def _accept(s):
- return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04"
+def _accept(prefix: bytes) -> bool:
+ return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04"
##
@@ -34,8 +34,10 @@ class McIdasImageFile(ImageFile.ImageFile):
format = "MCIDAS"
format_description = "McIdas area file"
- def _open(self):
+ def _open(self) -> None:
# parse area file directory
+ assert self.fp is not None
+
s = self.fp.read(256)
if not _accept(s) or len(s) != 256:
msg = "not an McIdas area file"
diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py
index f4e598ca3..b9e9243e5 100644
--- a/src/PIL/MpegImagePlugin.py
+++ b/src/PIL/MpegImagePlugin.py
@@ -14,6 +14,8 @@
#
from __future__ import annotations
+from io import BytesIO
+
from . import Image, ImageFile
from ._binary import i8
@@ -22,15 +24,15 @@ from ._binary import i8
class BitStream:
- def __init__(self, fp):
+ def __init__(self, fp: BytesIO) -> None:
self.fp = fp
self.bits = 0
self.bitbuffer = 0
- def next(self):
+ def next(self) -> int:
return i8(self.fp.read(1))
- def peek(self, bits):
+ def peek(self, bits: int) -> int:
while self.bits < bits:
c = self.next()
if c < 0:
@@ -40,13 +42,13 @@ class BitStream:
self.bits += 8
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1
- def skip(self, bits):
+ def skip(self, bits: int) -> None:
while self.bits < bits:
self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1))
self.bits += 8
self.bits = self.bits - bits
- def read(self, bits):
+ def read(self, bits: int) -> int:
v = self.peek(bits)
self.bits = self.bits - bits
return v
@@ -61,9 +63,10 @@ class MpegImageFile(ImageFile.ImageFile):
format = "MPEG"
format_description = "MPEG"
- def _open(self):
- s = BitStream(self.fp)
+ def _open(self) -> None:
+ assert self.fp is not None
+ s = BitStream(self.fp)
if s.read(32) != 0x1B3:
msg = "not an MPEG file"
raise SyntaxError(msg)
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index 77dac65b6..bb7e466a7 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -35,7 +35,7 @@ from ._binary import o16le as o16
# read MSP files
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:4] in [b"DanM", b"LinS"]
@@ -48,8 +48,10 @@ class MspImageFile(ImageFile.ImageFile):
format = "MSP"
format_description = "Windows Paint"
- def _open(self):
+ def _open(self) -> None:
# Header
+ assert self.fp is not None
+
s = self.fp.read(32)
if not _accept(s):
msg = "not an MSP file"
@@ -109,7 +111,9 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
+
img = io.BytesIO()
blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8))
try:
@@ -159,7 +163,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only)
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg)
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index a0515b302..1cd5c4a9d 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -27,8 +27,10 @@ class PcdImageFile(ImageFile.ImageFile):
format = "PCD"
format_description = "Kodak PhotoCD"
- def _open(self):
+ def _open(self) -> None:
# rough
+ assert self.fp is not None
+
self.fp.seek(2048)
s = self.fp.read(2048)
@@ -47,9 +49,11 @@ class PcdImageFile(ImageFile.ImageFile):
self._size = 768, 512 # FIXME: not correct for rotated images!
self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)]
- def load_end(self):
+ def load_end(self) -> None:
if self.tile_post_rotate:
# Handle rotated PCDs
+ assert self.im is not None
+
self.im = self.im.rotate(self.tile_post_rotate)
self._size = self.im.size
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 98ecefd05..3e0968a83 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -37,7 +37,7 @@ from ._binary import o16le as o16
logger = logging.getLogger(__name__)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
@@ -49,8 +49,10 @@ class PcxImageFile(ImageFile.ImageFile):
format = "PCX"
format_description = "Paintbrush"
- def _open(self):
+ def _open(self) -> None:
# header
+ assert self.fp is not None
+
s = self.fp.read(128)
if not _accept(s):
msg = "not a PCX file"
@@ -141,7 +143,7 @@ SAVE = {
}
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None:
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
@@ -199,6 +201,8 @@ def _save(im, fp, filename):
if im.mode == "P":
# colour palette
+ assert im.im is not None
+
fp.write(o8(12))
palette = im.im.getpalette("RGB", "RGB")
palette += b"\x00" * (768 - len(palette))
diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py
index af866feb3..887b6568b 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -27,7 +27,7 @@ from ._binary import i16le as i16
# helpers
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\200\350\000\000"
@@ -39,8 +39,10 @@ class PixarImageFile(ImageFile.ImageFile):
format = "PIXAR"
format_description = "PIXAR raster image"
- def _open(self):
+ def _open(self) -> None:
# assuming a 4-byte magic label
+ assert self.fp is not None
+
s = self.fp.read(4)
if not _accept(s):
msg = "not a PIXAR file"
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index e4ed93880..823f12492 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -378,7 +378,7 @@ class PngStream(ChunkStream):
}
def rewind(self):
- self.im_info = self.rewind_state["info"]
+ self.im_info = self.rewind_state["info"].copy()
self.im_tile = self.rewind_state["tile"]
self._seq_num = self.rewind_state["seq_num"]
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 25dbfa5b0..3e45ba95c 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -15,6 +15,9 @@
#
from __future__ import annotations
+import math
+from io import BytesIO
+
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
@@ -35,6 +38,7 @@ MODES = {
b"P6": "RGB",
# extensions
b"P0CMYK": "CMYK",
+ b"Pf": "F",
# PIL extensions (for test purposes only)
b"PyP": "P",
b"PyRGBA": "RGBA",
@@ -42,8 +46,8 @@ MODES = {
}
-def _accept(prefix):
- return prefix[0:1] == b"P" and prefix[1] in b"0123456y"
+def _accept(prefix: bytes) -> bool:
+ return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"
##
@@ -54,7 +58,9 @@ class PpmImageFile(ImageFile.ImageFile):
format = "PPM"
format_description = "Pbmplus image"
- def _read_magic(self):
+ def _read_magic(self) -> bytes:
+ assert self.fp is not None
+
magic = b""
# read until whitespace or longest available magic number
for _ in range(6):
@@ -64,7 +70,9 @@ class PpmImageFile(ImageFile.ImageFile):
magic += c
return magic
- def _read_token(self):
+ def _read_token(self) -> bytes:
+ assert self.fp is not None
+
token = b""
while len(token) <= 10: # read until next whitespace or limit of 10 characters
c = self.fp.read(1)
@@ -90,13 +98,16 @@ class PpmImageFile(ImageFile.ImageFile):
raise ValueError(msg)
return token
- def _open(self):
+ def _open(self) -> None:
+ assert self.fp is not None
+
magic_number = self._read_magic()
try:
mode = MODES[magic_number]
except KeyError:
msg = "not a PPM file"
raise SyntaxError(msg)
+ self._mode = mode
if magic_number in (b"P1", b"P4"):
self.custom_mimetype = "image/x-portable-bitmap"
@@ -105,40 +116,42 @@ class PpmImageFile(ImageFile.ImageFile):
elif magic_number in (b"P3", b"P6"):
self.custom_mimetype = "image/x-portable-pixmap"
- maxval = None
+ self._size = int(self._read_token()), int(self._read_token())
+
decoder_name = "raw"
if magic_number in (b"P1", b"P2", b"P3"):
decoder_name = "ppm_plain"
- for ix in range(3):
- token = int(self._read_token())
- if ix == 0: # token is the x size
- xsize = token
- elif ix == 1: # token is the y size
- ysize = token
- if mode == "1":
- self._mode = "1"
- rawmode = "1;I"
- break
- else:
- self._mode = rawmode = mode
- elif ix == 2: # token is maxval
- maxval = token
- if not 0 < maxval < 65536:
- msg = "maxval must be greater than 0 and less than 65536"
- raise ValueError(msg)
- if maxval > 255 and mode == "L":
- self._mode = "I"
- if decoder_name != "ppm_plain":
- # If maxval matches a bit depth, use the raw decoder directly
- if maxval == 65535 and mode == "L":
- rawmode = "I;16B"
- elif maxval != 255:
- decoder_name = "ppm"
+ args: str | tuple[str | int, ...]
+ if mode == "1":
+ args = "1;I"
+ elif mode == "F":
+ scale = float(self._read_token())
+ if scale == 0.0 or not math.isfinite(scale):
+ msg = "scale must be finite and non-zero"
+ raise ValueError(msg)
+ self.info["scale"] = abs(scale)
- args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval)
- self._size = xsize, ysize
- self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)]
+ rawmode = "F;32F" if scale < 0 else "F;32BF"
+ args = (rawmode, 0, -1)
+ else:
+ maxval = int(self._read_token())
+ if not 0 < maxval < 65536:
+ msg = "maxval must be greater than 0 and less than 65536"
+ raise ValueError(msg)
+ if maxval > 255 and mode == "L":
+ self._mode = "I"
+
+ rawmode = mode
+ if decoder_name != "ppm_plain":
+ # If maxval matches a bit depth, use the raw decoder directly
+ if maxval == 65535 and mode == "L":
+ rawmode = "I;16B"
+ elif maxval != 255:
+ decoder_name = "ppm"
+
+ args = rawmode if decoder_name == "raw" else (rawmode, maxval)
+ self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]
#
@@ -147,16 +160,19 @@ class PpmImageFile(ImageFile.ImageFile):
class PpmPlainDecoder(ImageFile.PyDecoder):
_pulls_fd = True
+ _comment_spans: bool
+
+ def _read_block(self) -> bytes:
+ assert self.fd is not None
- def _read_block(self):
return self.fd.read(ImageFile.SAFEBLOCK)
- def _find_comment_end(self, block, start=0):
+ def _find_comment_end(self, block: bytes, start: int = 0) -> int:
a = block.find(b"\n", start)
b = block.find(b"\r", start)
return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1)
- def _ignore_comments(self, block):
+ def _ignore_comments(self, block: bytes) -> bytes:
if self._comment_spans:
# Finish current comment
while block:
@@ -190,7 +206,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
break
return block
- def _decode_bitonal(self):
+ def _decode_bitonal(self) -> bytearray:
"""
This is a separate method because in the plain PBM format, all data tokens are
exactly one byte, so the inter-token whitespace is optional.
@@ -215,7 +231,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
invert = bytes.maketrans(b"01", b"\xFF\x00")
return data.translate(invert)
- def _decode_blocks(self, maxval):
+ def _decode_blocks(self, maxval: int) -> bytearray:
data = bytearray()
max_len = 10
out_byte_count = 4 if self.mode == "I" else 1
@@ -223,7 +239,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
bands = Image.getmodebands(self.mode)
total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
- half_token = False
+ half_token = b""
while len(data) != total_bytes:
block = self._read_block() # read next block
if not block:
@@ -237,7 +253,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
if half_token:
block = half_token + block # stitch half_token to new block
- half_token = False
+ half_token = b""
tokens = block.split()
@@ -255,15 +271,15 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
raise ValueError(msg)
value = int(token)
if value > maxval:
- msg = f"Channel value too large for this mode: {value}"
- raise ValueError(msg)
+ msg_str = f"Channel value too large for this mode: {value}"
+ raise ValueError(msg_str)
value = round(value / maxval * out_max)
data += o32(value) if self.mode == "I" else o8(value)
if len(data) == total_bytes: # finished!
break
return data
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
self._comment_spans = False
if self.mode == "1":
data = self._decode_bitonal()
@@ -279,7 +295,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
class PpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
+
data = bytearray()
maxval = self.args[-1]
in_byte_count = 1 if maxval < 256 else 2
@@ -306,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder):
# --------------------------------------------------------------------
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
if im.mode == "1":
rawmode, head = "1;I", b"P4"
elif im.mode == "L":
@@ -315,6 +333,8 @@ def _save(im, fp, filename):
rawmode, head = "I;16B", b"P5"
elif im.mode in ("RGB", "RGBA"):
rawmode, head = "RGB", b"P6"
+ elif im.mode == "F":
+ rawmode, head = "F;32F", b"Pf"
else:
msg = f"cannot write mode {im.mode} as PPM"
raise OSError(msg)
@@ -326,7 +346,10 @@ def _save(im, fp, filename):
fp.write(b"255\n")
else:
fp.write(b"65535\n")
- ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
+ elif head == b"Pf":
+ fp.write(b"-1.0\n")
+ row_order = -1 if im.mode == "F" else 1
+ ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])
#
@@ -339,6 +362,6 @@ Image.register_save(PpmImageFile.format, _save)
Image.register_decoder("ppm", PpmDecoder)
Image.register_decoder("ppm_plain", PpmPlainDecoder)
-Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
+Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index f9a10f610..ccf661ff1 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -24,13 +24,14 @@ from __future__ import annotations
import os
import struct
+from io import BytesIO
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return len(prefix) >= 2 and i16(prefix) == 474
@@ -52,8 +53,10 @@ class SgiImageFile(ImageFile.ImageFile):
format = "SGI"
format_description = "SGI Image File Format"
- def _open(self):
+ def _open(self) -> None:
# HEAD
+ assert self.fp is not None
+
headlen = 512
s = self.fp.read(headlen)
@@ -122,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile):
]
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode"
raise ValueError(msg)
@@ -168,8 +171,8 @@ def _save(im, fp, filename):
# Maximum Byte value (255 = 8bits per pixel)
pinmax = 255
# Image name (79 characters max, truncated below in write)
- img_name = os.path.splitext(os.path.basename(filename))[0]
- img_name = img_name.encode("ascii", "ignore")
+ filename = os.path.basename(filename)
+ img_name = os.path.splitext(filename)[0].encode("ascii", "ignore")
# Standard representation of pixel in the file
colormap = 0
fp.write(struct.pack(">h", magic_number))
@@ -201,7 +204,10 @@ def _save(im, fp, filename):
class SGI16Decoder(ImageFile.PyDecoder):
_pulls_fd = True
- def decode(self, buffer):
+ def decode(self, buffer: bytes) -> tuple[int, int]:
+ assert self.fd is not None
+ assert self.im is not None
+
rawmode, stride, orientation = self.args
pagesize = self.state.xsize * self.state.ysize
zsize = len(self.mode)
diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py
index 11ce3dfef..4e098474a 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -21,7 +21,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i32be as i32
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return len(prefix) >= 4 and i32(prefix) == 0x59A66A95
@@ -33,7 +33,7 @@ class SunImageFile(ImageFile.ImageFile):
format = "SUN"
format_description = "Sun Raster File"
- def _open(self):
+ def _open(self) -> None:
# The Sun Raster file header is 32 bytes in length
# and has the following format:
@@ -49,6 +49,8 @@ class SunImageFile(ImageFile.ImageFile):
# DWORD ColorMapLength; /* Size of the color map in bytes */
# } SUNRASTER;
+ assert self.fp is not None
+
# HEAD
s = self.fp.read(32)
if not _accept(s):
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 65c7484f7..584932d2c 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import warnings
+from io import BytesIO
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@@ -49,8 +50,10 @@ class TgaImageFile(ImageFile.ImageFile):
format = "TGA"
format_description = "Targa"
- def _open(self):
+ def _open(self) -> None:
# process header
+ assert self.fp is not None
+
s = self.fp.read(18)
id_len = s[0]
@@ -151,8 +154,9 @@ class TgaImageFile(ImageFile.ImageFile):
except KeyError:
pass # cannot decode
- def load_end(self):
+ def load_end(self) -> None:
if self._flip_horizontally:
+ assert self.im is not None
self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
@@ -171,7 +175,7 @@ SAVE = {
}
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e:
@@ -194,6 +198,7 @@ def _save(im, fp, filename):
warnings.warn("id_section has been trimmed to 255 characters")
if colormaptype:
+ assert im.im is not None
palette = im.im.getpalette("RGB", "BGR")
colormaplength, colormapentry = len(palette) // 3, 24
else:
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index 47ba1c548..c84adaca2 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -33,7 +33,7 @@ for r in range(8):
)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix[:6] == _MAGIC
@@ -45,8 +45,10 @@ class XVThumbImageFile(ImageFile.ImageFile):
format = "XVThumb"
format_description = "XV thumbnail image"
- def _open(self):
+ def _open(self) -> None:
# check magic
+ assert self.fp is not None
+
if not _accept(self.fp.read(6)):
msg = "not an XV thumbnail file"
raise SyntaxError(msg)
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index 566acbfe5..0291e2858 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -21,6 +21,7 @@
from __future__ import annotations
import re
+from io import BytesIO
from . import Image, ImageFile
@@ -36,7 +37,7 @@ xbm_head = re.compile(
)
-def _accept(prefix):
+def _accept(prefix: bytes) -> bool:
return prefix.lstrip()[:7] == b"#define"
@@ -48,7 +49,9 @@ class XbmImageFile(ImageFile.ImageFile):
format = "XBM"
format_description = "X11 Bitmap"
- def _open(self):
+ def _open(self) -> None:
+ assert self.fp is not None
+
m = xbm_head.match(self.fp.read(512))
if not m:
@@ -67,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
-def _save(im, fp, filename):
+def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg)
diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_imagingmath.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingmorph.pyi b/src/PIL/_imagingmorph.pyi
new file mode 100644
index 000000000..b0235555d
--- /dev/null
+++ b/src/PIL/_imagingmorph.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 68c66ac2c..6e24fcf95 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) {
image = PyObject_CallFunction(fill, "ii", width, height);
if (image == Py_None) {
PyMem_Del(glyph_info);
- return Py_BuildValue("ii", 0, 0);
+ return Py_BuildValue("N(ii)", image, 0, 0);
} else if (image == NULL) {
PyMem_Del(glyph_info);
return NULL;
@@ -894,7 +894,7 @@ font_render(FontObject *self, PyObject *args) {
y_offset -= stroke_width;
if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info);
- return Py_BuildValue("ii", x_offset, y_offset);
+ return Py_BuildValue("N(ii)", image, x_offset, y_offset);
}
if (stroke_width) {
@@ -1130,18 +1130,12 @@ font_render(FontObject *self, PyObject *args) {
if (bitmap_converted_ready) {
FT_Bitmap_Done(library, &bitmap_converted);
}
- Py_DECREF(image);
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
- return Py_BuildValue("ii", x_offset, y_offset);
+ return Py_BuildValue("N(ii)", image, x_offset, y_offset);
glyph_error:
- if (im->destroy) {
- im->destroy(im);
- }
- if (im->image) {
- free(im->image);
- }
+ Py_DECREF(image);
if (stroker != NULL) {
FT_Done_Glyph(glyph);
}
diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c
index f23245405..e37301df7 100644
--- a/src/libImaging/GifEncode.c
+++ b/src/libImaging/GifEncode.c
@@ -105,7 +105,7 @@ encode_loop:
st->head = st->codes[st->probe] >> 20;
goto encode_loop;
} else {
- /* Reprobe decrement must be nonzero and relatively prime to table
+ /* Reprobe decrement must be non-zero and relatively prime to table
* size. So, any odd positive number for power-of-2 size. */
if ((st->probe -= ((st->tail << 2) | 1)) < 0) {
st->probe += TABLE_SIZE;
diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h
index 98eaac28d..7cdba9022 100644
--- a/src/libImaging/Jpeg.h
+++ b/src/libImaging/Jpeg.h
@@ -74,7 +74,7 @@ typedef struct {
/* Optimize Huffman tables (slow) */
int optimize;
- /* Disable automatic conversion of RGB images to YCbCr if nonzero */
+ /* Disable automatic conversion of RGB images to YCbCr if non-zero */
int keep_rgb;
/* Stream type (0=full, 1=tables only, 2=image only) */
diff --git a/tox.ini b/tox.ini
index d89d017e4..fb6746ce7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -33,6 +33,7 @@ commands =
[testenv:mypy]
skip_install = true
deps =
+ ipython
mypy==1.7.1
numpy
extras =
diff --git a/winbuild/build.rst b/winbuild/build.rst
index a8e4ebaa6..cd3b559e7 100644
--- a/winbuild/build.rst
+++ b/winbuild/build.rst
@@ -27,7 +27,7 @@ Download and install:
* `Ninja `_
(optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component)
-* x86/x64: `Netwide Assembler (NASM) `_
+* x86/AMD64: `Netwide Assembler (NASM) `_
Any version of Visual Studio 2017 or newer should be supported,
including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019.
@@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build::
usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD]
[--depends PILLOW_DEPS]
- [--architecture {x86,x64,ARM64}] [--nmake]
+ [--architecture {x86,AMD64,ARM64}] [--nmake]
[--no-imagequant] [--no-fribidi]
Download and generate build scripts for Pillow dependencies.
@@ -55,7 +55,7 @@ Run ``build_prepare.py`` to configure the build::
--depends PILLOW_DEPS
directory used to store cached dependencies (default:
'winbuild\depends')
- --architecture {x86,x64,ARM64}
+ --architecture {x86,AMD64,ARM64}
build architecture (default: same as host Python)
--nmake build dependencies using NMake instead of Ninja
--no-imagequant skip GPL-licensed optional dependency libimagequant
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 8e3757ca8..df33ea493 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -105,7 +105,7 @@ SF_PROJECTS = "https://sourceforge.net/projects"
ARCHITECTURES = {
"x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"},
- "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"},
+ "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"},
"ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"},
}
@@ -174,23 +174,22 @@ DEPS = {
"filename": "libwebp-1.3.2.tar.gz",
"dir": "libwebp-1.3.2",
"license": "COPYING",
+ "patch": {
+ r"src\enc\picture_csp_enc.c": {
+ # link against libsharpyuv.lib
+ '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501
+ }
+ },
"build": [
- cmd_rmdir(r"output\release-static"), # clean
- cmd_nmake(
- "Makefile.vc",
- "all",
- [
- "CFG=release-static",
- "RTLIBCFG=dynamic",
- "OBJDIR=output",
- "ARCH={architecture}",
- "LIBWEBP_BASENAME=webp",
- ],
+ *cmds_cmake(
+ "webp webpdemux webpmux",
+ "-DBUILD_SHARED_LIBS:BOOL=OFF",
+ "-DWEBP_LINK_STATIC:BOOL=OFF",
),
cmd_mkdir(r"{inc_dir}\webp"),
cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"),
],
- "libs": [r"output\release-static\{architecture}\lib\*.lib"],
+ "libs": [r"libsharpyuv.lib", r"libwebp*.lib"],
},
"libtiff": {
"url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz",
@@ -203,8 +202,8 @@ DEPS = {
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501
},
r"libtiff\tif_webp.c": {
- # link against webp.lib
- "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501
+ # link against libwebp.lib
+ "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501
},
r"test\CMakeLists.txt": {
"add_executable(test_write_read_tags ../placeholder.h)": "",
@@ -217,6 +216,7 @@ DEPS = {
*cmds_cmake(
"tiff",
"-DBUILD_SHARED_LIBS:BOOL=OFF",
+ "-DWebP_LIBRARY=libwebp",
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
)
],
@@ -651,7 +651,7 @@ if __name__ == "__main__":
(
"ARM64"
if platform.machine() == "ARM64"
- else ("x86" if struct.calcsize("P") == 4 else "x64")
+ else ("x86" if struct.calcsize("P") == 4 else "AMD64")
),
),
help="build architecture (default: same as host Python)",