diff --git a/.appveyor.yml b/.appveyor.yml
index 0f5dea9c515..4c5a7f9ee47 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/.coveragerc b/.coveragerc
index f71b6b1a281..46df3f90d27 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,15 +2,14 @@
[report]
# Regexes for lines to exclude from consideration
-exclude_lines =
- # Have to re-enable the standard pragma:
- pragma: no cover
-
- # Don't complain if non-runnable code isn't run:
+exclude_also =
+ # Don't complain if non-runnable code isn't run
if 0:
if __name__ == .__main__.:
# Don't complain about debug code
if DEBUG:
+ # Don't complain about compatibility code for missing optional dependencies
+ except ImportError
[run]
omit =
diff --git a/.github/problem-matchers/gcc.json b/.github/problem-matchers/gcc.json
new file mode 100644
index 00000000000..8e2866afe23
--- /dev/null
+++ b/.github/problem-matchers/gcc.json
@@ -0,0 +1,18 @@
+{
+ "__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
+ "problemMatcher": [
+ {
+ "owner": "gcc-problem-matcher",
+ "pattern": [
+ {
+ "regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
+ "file": 1,
+ "line": 2,
+ "column": 3,
+ "severity": 4,
+ "message": 5
+ }
+ ]
+ }
+ ]
+}
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
index 4d855469a12..3711d91f0d5 100644
--- a/.github/release-drafter.yml
+++ b/.github/release-drafter.yml
@@ -13,6 +13,8 @@ categories:
label: "Removal"
- title: "Testing"
label: "Testing"
+ - title: "Type hints"
+ label: "Type hints"
exclude-labels:
- "changelog: skip"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 9069fc615f3..cc4760288e5 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- name: pre-commit cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 32ac6f65e76..9c3eb092417 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:
@@ -97,7 +95,7 @@ jobs:
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
- name: pip cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index eb27b4bf75b..3bb6856f6e8 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 115c2e9bebc..cdd51e2bb3f 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 86cd5b5fa1e..8cad7a8b281 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:
@@ -89,7 +89,7 @@ jobs:
- name: Cache build
id: build-cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: winbuild\build
key:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aa0e2513825..05f78704bcd 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:
@@ -86,6 +84,10 @@ jobs:
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
+ - name: Register gcc problem matcher
+ if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
+ run: echo "::add-matcher::.github/problem-matchers/gcc.json"
+
- name: Build
run: |
.ci/build.sh
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 060fc497ea7..1140aaaad52 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:
@@ -39,18 +96,18 @@ jobs:
include:
- name: "macOS x86_64"
os: macos-latest
- archs: x86_64
+ cibw_arch: x86_64
macosx_deployment_target: "10.10"
- name: "macOS arm64"
os: macos-latest
- archs: arm64
+ cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
os: ubuntu-latest
- archs: x86_64
+ cibw_arch: x86_64
- name: "manylinux_2_28 x86_64"
os: ubuntu-latest
- archs: x86_64
+ cibw_arch: x86_64
build: "*manylinux*"
manylinux: "manylinux_2_28"
steps:
@@ -62,12 +119,15 @@ 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.archs }}
+ CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
@@ -77,22 +137,19 @@ jobs:
- uses: actions/upload-artifact@v4
with:
- name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
+ name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
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
@@ -143,6 +202,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
+ CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
@@ -156,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:
@@ -186,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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d1c4b801527..6adc75b4902 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.7
+ rev: v0.1.9
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 23.12.0
+ rev: 23.12.1
hooks:
- id: black
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 8f8250809d7..00000000000
--- 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 df4e11e0e48..62ae2a68bbc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,69 @@
Changelog (Pillow)
==================
-10.2.0 (unreleased)
+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)
+-------------------
+
+- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553
+ [bgilbert, radarhere]
+
+- Trim glyph size in ImageFont.getmask() #7669, #7672
+ [radarhere, nulano]
+
+- Deprecate IptcImagePlugin helpers #7664
+ [nulano, hugovk, radarhere]
+
+- Allow uncompressed TIFF images to be saved in chunks #7650
+ [radarhere]
+
+- Concatenate multiple JPEG EXIF markers #7496
+ [radarhere]
+
+- Changed IPTC tile tuple to match other plugins #7661
+ [radarhere]
+
+- Do not assign new fp attribute when exiting context manager #7566
+ [radarhere]
+
+- Support arbitrary masks for uncompressed RGB DDS images #7589
+ [radarhere, akx]
+
+- Support setting ROWSPERSTRIP tag #7654
+ [radarhere]
+
+- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
+ [radarhere]
+
+- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657
+ [hugovk]
+
+- Restricted environment keys for ImageMath.eval() #7655
+ [wiredfool, radarhere]
+
+- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641
+ [hugovk, radarhere]
+
- Fix incorrect color blending for overlapping glyphs #7497
[ZachNagengast, nulano, radarhere]
diff --git a/LICENSE b/LICENSE
index cf65e86d734..0069eb5bcec 100644
--- a/LICENSE
+++ b/LICENSE
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors.
+ Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License:
diff --git a/README.md b/README.md
index e11bd2faa1d..6ca870166a1 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
-
-
str | None:
+ if uploader == "show":
+ # local img.show for errors.
+ a.show()
+ b.show()
+ elif uploader == "github_actions":
+ dir_errors = os.path.join(os.path.dirname(__file__), "errors")
+ os.makedirs(dir_errors, exist_ok=True)
+ tmpdir = tempfile.mkdtemp(dir=dir_errors)
+ a.save(os.path.join(tmpdir, "a.png"))
+ b.save(os.path.join(tmpdir, "b.png"))
+ return tmpdir
+ elif uploader == "aws":
+ return test_image_results.upload(a, b)
+ return None
+
+
+def convert_to_comparable(
+ a: Image.Image, b: Image.Image
+) -> tuple[Image.Image, Image.Image]:
new_a, new_b = a, b
if a.mode == "P":
new_a = Image.new("L", a.size)
@@ -67,14 +66,18 @@ def convert_to_comparable(a, b):
return new_a, new_b
-def assert_deep_equal(a, b, msg=None):
+def assert_deep_equal(
+ a: Sequence[Any], b: Sequence[Any], msg: str | None = None
+) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
assert a == b, msg
-def assert_image(im, mode, size, msg=None):
+def assert_image(
+ im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None
+) -> None:
if mode is not None:
assert im.mode == mode, (
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
@@ -86,28 +89,32 @@ def assert_image(im, mode, size, msg=None):
)
-def assert_image_equal(a, b, msg=None):
+def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
if a.tobytes() != b.tobytes():
- if HAS_UPLOADER:
- try:
- url = test_image_results.upload(a, b)
+ try:
+ url = upload(a, b)
+ if url:
logger.error("URL for test images: %s", url)
- except Exception:
- pass
+ except Exception:
+ pass
pytest.fail(msg or "got different content")
-def assert_image_equal_tofile(a, filename, msg=None, mode=None):
+def assert_image_equal_tofile(
+ a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
+) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_equal(a, img, msg)
-def assert_image_similar(a, b, epsilon, msg=None):
+def assert_image_similar(
+ a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None
+) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
@@ -125,55 +132,68 @@ def assert_image_similar(a, b, epsilon, msg=None):
+ f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
)
except Exception as e:
- if HAS_UPLOADER:
- try:
- url = test_image_results.upload(a, b)
+ try:
+ url = upload(a, b)
+ if url:
logger.exception("URL for test images: %s", url)
- except Exception:
- pass
+ except Exception:
+ pass
raise e
-def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None):
+def assert_image_similar_tofile(
+ a: Image.Image,
+ filename: str,
+ epsilon: float,
+ msg: str | None = None,
+ mode: str | None = None,
+) -> None:
with Image.open(filename) as img:
if mode:
img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg)
-def assert_all_same(items, msg=None):
+def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg
-def assert_not_all_same(items, msg=None):
+def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg
-def assert_tuple_approx_equal(actuals, targets, threshold, msg):
+def assert_tuple_approx_equal(
+ actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str
+) -> None:
"""Tests if actuals has values within threshold from targets"""
- value = True
for i, target in enumerate(targets):
- value *= target - threshold <= actuals[i] <= target + threshold
-
- assert value, msg + ": " + repr(actuals) + " != " + repr(targets)
+ if not (target - threshold <= actuals[i] <= target + threshold):
+ pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
-def skip_unless_feature(feature):
+def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason)
-def skip_unless_feature_version(feature, version_required, reason=None):
+def skip_unless_feature_version(
+ feature: str, required: str, reason: str | None = None
+) -> pytest.MarkDecorator:
if not features.check(feature):
return pytest.mark.skip(f"{feature} not available")
if reason is None:
- reason = f"{feature} is older than {version_required}"
- version_required = parse_version(version_required)
+ reason = f"{feature} is older than {required}"
+ version_required = parse_version(required)
version_available = parse_version(features.version(feature))
return pytest.mark.skipif(version_available < version_required, reason=reason)
-def mark_if_feature_version(mark, feature, version_blacklist, reason=None):
+def mark_if_feature_version(
+ mark: pytest.MarkDecorator,
+ feature: str,
+ version_blacklist: str,
+ reason: str | None = None,
+) -> pytest.MarkDecorator:
if not features.check(feature):
return pytest.mark.pil_noop_mark()
if reason is None:
@@ -194,7 +214,7 @@ class PillowLeakTestCase:
iterations = 100 # count
mem_limit = 512 # k
- def _get_mem_usage(self):
+ def _get_mem_usage(self) -> float:
"""
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
between macOS and Linux rss reporting
@@ -216,7 +236,7 @@ def _get_mem_usage(self):
# This is the maximum resident set size used (in kilobytes).
return mem # Kb
- def _test_leak(self, core):
+ def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage()
for cycle in range(self.iterations):
core()
@@ -228,17 +248,17 @@ def _test_leak(self, core):
# helpers
-def fromstring(data):
+def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data))
-def tostring(im, string_format, **options):
+def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
out = BytesIO()
im.save(out, string_format, **options)
return out.getvalue()
-def hopper(mode=None, cache={}):
+def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
if mode is None:
# Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors
@@ -259,29 +279,31 @@ def hopper(mode=None, cache={}):
return im.copy()
-def djpeg_available():
+def djpeg_available() -> bool:
if shutil.which("djpeg"):
try:
subprocess.check_call(["djpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
+ return False
-def cjpeg_available():
+def cjpeg_available() -> bool:
if shutil.which("cjpeg"):
try:
subprocess.check_call(["cjpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
+ return False
-def netpbm_available():
+def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
-def magick_command():
+def magick_command() -> list[str] | None:
if sys.platform == "win32":
magickhome = os.environ.get("MAGICK_HOME")
if magickhome:
@@ -298,47 +320,48 @@ def magick_command():
return imagemagick
if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick
+ return None
-def on_appveyor():
+def on_appveyor() -> bool:
return "APPVEYOR" in os.environ
-def on_github_actions():
+def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ
-def on_ci():
+def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ
-def is_big_endian():
+def is_big_endian() -> bool:
return sys.byteorder == "big"
-def is_ppc64le():
+def is_ppc64le() -> bool:
import platform
return platform.machine() == "ppc64le"
-def is_win32():
+def is_win32() -> bool:
return sys.platform.startswith("win32")
-def is_pypy():
+def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info")
-def is_mingw():
+def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw"
class CachedProperty:
- def __init__(self, func):
+ def __init__(self, func: Callable[[Any], None]) -> None:
self.func = func
- def __get__(self, instance, cls=None):
+ def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
result = instance.__dict__[self.func.__name__] = self.func(instance)
return result
diff --git a/Tests/images/apng/different_durations.png b/Tests/images/apng/different_durations.png
new file mode 100644
index 00000000000..984254b8e56
Binary files /dev/null and b/Tests/images/apng/different_durations.png differ
diff --git a/Tests/images/bgr15.dds b/Tests/images/bgr15.dds
new file mode 100644
index 00000000000..ba3bbddcae4
Binary files /dev/null and b/Tests/images/bgr15.dds differ
diff --git a/Tests/images/bgr15.png b/Tests/images/bgr15.png
new file mode 100644
index 00000000000..a15ab5ad256
Binary files /dev/null and b/Tests/images/bgr15.png differ
diff --git a/Tests/images/hopper.pfm b/Tests/images/hopper.pfm
new file mode 100644
index 00000000000..b5766156401
Binary files /dev/null and b/Tests/images/hopper.pfm differ
diff --git a/Tests/images/hopper_be.pfm b/Tests/images/hopper_be.pfm
new file mode 100644
index 00000000000..93c75e26fda
Binary files /dev/null and b/Tests/images/hopper_be.pfm differ
diff --git a/Tests/images/multiple_exif.jpg b/Tests/images/multiple_exif.jpg
new file mode 100644
index 00000000000..32e0aa301a9
Binary files /dev/null and b/Tests/images/multiple_exif.jpg differ
diff --git a/Tests/images/unsupported_bitcount_luminance.dds b/Tests/images/unsupported_bitcount.dds
similarity index 100%
rename from Tests/images/unsupported_bitcount_luminance.dds
rename to Tests/images/unsupported_bitcount.dds
diff --git a/Tests/images/unsupported_bitcount_rgb.dds b/Tests/images/unsupported_bitcount_rgb.dds
deleted file mode 100644
index 77d527507f5..00000000000
Binary files a/Tests/images/unsupported_bitcount_rgb.dds and /dev/null differ
diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py
index 024117c56d0..8788d7021d3 100755
--- a/Tests/oss-fuzz/fuzz_font.py
+++ b/Tests/oss-fuzz/fuzz_font.py
@@ -1,7 +1,5 @@
#!/usr/bin/python3
-from __future__ import annotations
-
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,7 +23,7 @@
import fuzzers
-def TestOneInput(data):
+def TestOneInput(data: bytes) -> None:
try:
fuzzers.fuzz_font(data)
except Exception:
@@ -34,7 +32,7 @@ def TestOneInput(data):
pass
-def main():
+def main() -> None:
fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py
index c1ab42e5651..e6e99d415a6 100644
--- a/Tests/oss-fuzz/fuzz_pillow.py
+++ b/Tests/oss-fuzz/fuzz_pillow.py
@@ -1,7 +1,5 @@
#!/usr/bin/python3
-from __future__ import annotations
-
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,7 +23,7 @@
import fuzzers
-def TestOneInput(data):
+def TestOneInput(data: bytes) -> None:
try:
fuzzers.fuzz_image(data)
except Exception:
@@ -34,7 +32,7 @@ def TestOneInput(data):
pass
-def main():
+def main() -> None:
fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py
index 3f3c1e38833..d6c1fab714b 100644
--- a/Tests/oss-fuzz/fuzzers.py
+++ b/Tests/oss-fuzz/fuzzers.py
@@ -1,22 +1,23 @@
from __future__ import annotations
+
import io
import warnings
from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont
-def enable_decompressionbomb_error():
+def enable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True
warnings.filterwarnings("ignore")
warnings.simplefilter("error", Image.DecompressionBombWarning)
-def disable_decompressionbomb_error():
+def disable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False
warnings.resetwarnings()
-def fuzz_image(data):
+def fuzz_image(data: bytes) -> None:
# This will fail on some images in the corpus, as we have many
# invalid images in the test suite.
with Image.open(io.BytesIO(data)) as im:
@@ -25,7 +26,7 @@ def fuzz_image(data):
im.save(io.BytesIO(), "BMP")
-def fuzz_font(data):
+def fuzz_font(data: bytes) -> None:
wrapper = io.BytesIO(data)
try:
font = ImageFont.truetype(wrapper)
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index 68834045a52..459cc1a3724 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import subprocess
import sys
@@ -23,7 +24,7 @@
"path",
subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"),
)
-def test_fuzz_images(path):
+def test_fuzz_images(path: str) -> None:
fuzzers.enable_decompressionbomb_error()
try:
with open(path, "rb") as f:
@@ -54,7 +55,7 @@ def test_fuzz_images(path):
@pytest.mark.parametrize(
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
)
-def test_fuzz_fonts(path):
+def test_fuzz_fonts(path: str) -> None:
if not path:
return
with open(path, "rb") as f:
diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py
index c582dfad3e4..f64216bca8e 100644
--- a/Tests/test_000_sanity.py
+++ b/Tests/test_000_sanity.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
diff --git a/Tests/test_binary.py b/Tests/test_binary.py
index 62da2663668..41fb93fcf48 100644
--- a/Tests/test_binary.py
+++ b/Tests/test_binary.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import _binary
diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py
index bed8dc3a899..0da41e85845 100644
--- a/Tests/test_bmp_reference.py
+++ b/Tests/test_bmp_reference.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import warnings
diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py
index e798cba3d4f..461e6aaacb3 100644
--- a/Tests/test_box_blur.py
+++ b/Tests/test_box_blur.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py
index 448ba2fac80..fcd1169ef81 100644
--- a/Tests/test_color_lut.py
+++ b/Tests/test_color_lut.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from array import array
import pytest
diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py
index 5275652f66a..d3f76fdb1b9 100644
--- a/Tests/test_core_resources.py
+++ b/Tests/test_core_resources.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
import pytest
diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py
index 391948d40c7..d3049eff124 100644
--- a/Tests/test_decompression_bomb.py
+++ b/Tests/test_decompression_bomb.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py
index d45a6603c54..6c7f509a795 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import _deprecate
diff --git a/Tests/test_features.py b/Tests/test_features.py
index 8f0e4b4184a..b90c1d25f2b 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import re
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index b676c72ada6..b24ac154723 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -739,3 +740,12 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat
)
with Image.open(test_file) as reloaded:
assert reloaded.mode == mode
+
+
+def test_apng_repeated_seeks_give_correct_info() -> 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_blp.py b/Tests/test_file_blp.py
index 4c1e38d1ddf..27ff7ab6640 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index 4cc92c5f684..225fb28ba51 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import pytest
diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py
index 5780232a2d6..45081832e68 100644
--- a/Tests/test_file_bufrstub.py
+++ b/Tests/test_file_bufrstub.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import BufrStubImagePlugin, Image
diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py
index 0da5d3824b0..95a5b2337d8 100644
--- a/Tests/test_file_container.py
+++ b/Tests/test_file_container.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import ContainerIO, Image
diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py
index 08c3257f9d1..27b2bc91489 100644
--- a/Tests/test_file_cur.py
+++ b/Tests/test_file_cur.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import CurImagePlugin, Image
diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py
index 25e4badbc92..cba7c10bf76 100644
--- a/Tests/test_file_dcx.py
+++ b/Tests/test_file_dcx.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index 2d60fbb6460..7064b74c07b 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -1,5 +1,6 @@
"""Test DdsImagePlugin"""
from __future__ import annotations
+
from io import BytesIO
import pytest
@@ -32,6 +33,7 @@
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
+TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@@ -249,6 +251,7 @@ def test_dx10_r8g8b8a8_unorm_srgb():
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
+ ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
],
)
@@ -341,16 +344,9 @@ def test_palette():
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
-@pytest.mark.parametrize(
- "test_file",
- (
- "Tests/images/unsupported_bitcount_rgb.dds",
- "Tests/images/unsupported_bitcount_luminance.dds",
- ),
-)
-def test_unsupported_bitcount(test_file):
+def test_unsupported_bitcount():
with pytest.raises(OSError):
- with Image.open(test_file):
+ with Image.open("Tests/images/unsupported_bitcount.dds"):
pass
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index c479c384a76..8def9a43511 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import pytest
@@ -270,7 +271,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 +293,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_fits.py b/Tests/test_file_fits.py
index 1383f9c5ca3..7444eb673cb 100644
--- a/Tests/test_file_fits.py
+++ b/Tests/test_file_fits.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index 10bf36cc290..00377e0c92d 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py
index af3b7981561..d710070c01b 100644
--- a/Tests/test_file_fpx.py
+++ b/Tests/test_file_fpx.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py
index a494c8029c9..0f9154e3d09 100644
--- a/Tests/test_file_ftex.py
+++ b/Tests/test_file_ftex.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import FtexImagePlugin, Image
diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py
index 7dfe0539673..d84004e1483 100644
--- a/Tests/test_file_gbr.py
+++ b/Tests/test_file_gbr.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import GbrImagePlugin, Image
diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py
index ec80c54a122..e7db54fb44c 100644
--- a/Tests/test_file_gd.py
+++ b/Tests/test_file_gd.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import GdImageFile, UnidentifiedImageError
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 33b4c0f1b15..a0dd6007849 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
from io import BytesIO
diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py
index d5be46dc39e..ceea1edd34c 100644
--- a/Tests/test_file_gimpgradient.py
+++ b/Tests/test_file_gimpgradient.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import GimpGradientFile, ImagePalette
diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py
index 775d3b7cdae..28855c28acc 100644
--- a/Tests/test_file_gimppalette.py
+++ b/Tests/test_file_gimppalette.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL.GimpPaletteFile import GimpPaletteFile
diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py
index d962e85a436..a4ce6dde674 100644
--- a/Tests/test_file_gribstub.py
+++ b/Tests/test_file_gribstub.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import GribStubImagePlugin, Image
diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py
index 9c776b712ee..72764461730 100644
--- a/Tests/test_file_hdf5stub.py
+++ b/Tests/test_file_hdf5stub.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Hdf5StubImagePlugin, Image
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
index c62fffc5be8..314fa800868 100644
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import os
import warnings
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index de9fa353adb..99b3048d1e0 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import os
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index 0cb26d06a51..a031b3e887c 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import filecmp
import warnings
diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py
index 3db4885586a..aa13d4407bc 100644
--- a/Tests/test_file_imt.py
+++ b/Tests/test_file_imt.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import pytest
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index d0ecde393a4..a2c50ecefdf 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
from io import BytesIO, StringIO
@@ -6,11 +7,23 @@
from PIL import Image, IptcImagePlugin
-from .helper import hopper
+from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/iptc.jpg"
+def test_open():
+ expected = Image.new("L", (1, 1))
+
+ f = BytesIO(
+ b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
+ b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
+ )
+ with Image.open(f) as im:
+ assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
+ assert_image_equal(im, expected)
+
+
def test_getiptcinfo_jpg_none():
# Arrange
with hopper() as im:
@@ -78,24 +91,28 @@ def test_i():
c = b"a"
# Act
- ret = IptcImagePlugin.i(c)
+ with pytest.warns(DeprecationWarning):
+ ret = IptcImagePlugin.i(c)
# Assert
assert ret == 97
-def test_dump():
+def test_dump(monkeypatch):
# Arrange
c = b"abc"
# Temporarily redirect stdout
- old_stdout = sys.stdout
- sys.stdout = mystdout = StringIO()
+ mystdout = StringIO()
+ monkeypatch.setattr(sys, "stdout", mystdout)
# Act
- IptcImagePlugin.dump(c)
-
- # Reset stdout
- sys.stdout = old_stdout
+ with pytest.warns(DeprecationWarning):
+ IptcImagePlugin.dump(c)
# Assert
assert mystdout.getvalue() == "61 62 63 \n"
+
+
+def test_pad_deprecation():
+ with pytest.warns(DeprecationWarning):
+ assert IptcImagePlugin.PAD == b"\0\0\0\0"
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index ffaea6296ef..232e51f9126 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import re
import warnings
@@ -142,6 +143,19 @@ def test_cmyk(self):
)
assert k > 0.9
+ def test_rgb(self):
+ def getchannels(im):
+ return tuple(v[0] for v in im.layer)
+
+ im = hopper()
+ im_ycbcr = self.roundtrip(im)
+ assert getchannels(im_ycbcr) == (1, 2, 3)
+ assert_image_similar(im, im_ycbcr, 17)
+
+ im_rgb = self.roundtrip(im, keep_rgb=True)
+ assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B"))
+ assert_image_similar(im, im_rgb, 12)
+
@pytest.mark.parametrize(
"test_image_path",
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
@@ -423,25 +437,28 @@ def getsampling(im):
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
# experimental API
- im = self.roundtrip(hopper(), subsampling=-1) # default
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=0) # 4:4:4
- assert getsampling(im) == (1, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=1) # 4:2:2
- assert getsampling(im) == (2, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=2) # 4:2:0
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling=3) # default (undefined)
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
-
- im = self.roundtrip(hopper(), subsampling="4:4:4")
- assert getsampling(im) == (1, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling="4:2:2")
- assert getsampling(im) == (2, 1, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling="4:2:0")
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
- im = self.roundtrip(hopper(), subsampling="4:1:1")
- assert getsampling(im) == (2, 2, 1, 1, 1, 1)
+ for subsampling in (-1, 3): # (default, invalid)
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (2, 2, 1, 1, 1, 1)
+ for subsampling in (0, "4:4:4"):
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (1, 1, 1, 1, 1, 1)
+ for subsampling in (1, "4:2:2"):
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (2, 1, 1, 1, 1, 1)
+ for subsampling in (2, "4:2:0", "4:1:1"):
+ im = self.roundtrip(hopper(), subsampling=subsampling)
+ assert getsampling(im) == (2, 2, 1, 1, 1, 1)
+
+ # RGB colorspace
+ for subsampling in (-1, 0, "4:4:4"):
+ # "4:4:4" doesn't really make sense for RGB, but the conversion
+ # to an integer happens at a higher level
+ im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
+ assert getsampling(im) == (1, 1, 1, 1, 1, 1)
+ for subsampling in (1, "4:2:2", 2, "4:2:0", 3):
+ with pytest.raises(OSError):
+ self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling)
with pytest.raises(TypeError):
self.roundtrip(hopper(), subsampling="1:1:1")
@@ -840,6 +857,10 @@ def test_ifd_offset_exif(self):
# Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09"
+ def test_multiple_exif(self):
+ with Image.open("Tests/images/multiple_exif.jpg") as im:
+ assert im.info["exif"] == b"Exif\x00\x00firstsecond"
+
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index aaa4104e57c..94b02c9ff52 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import re
from io import BytesIO
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 65adf449dcc..494253c87c1 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import base64
import io
import itertools
diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py
index 9501c55a6b8..171e4a3f866 100644
--- a/Tests/test_file_libtiff_small.py
+++ b/Tests/test_file_libtiff_small.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
from PIL import Image
diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py
index 4b31aaa7857..73eba5cc861 100644
--- a/Tests/test_file_mcidas.py
+++ b/Tests/test_file_mcidas.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, McIdasImagePlugin
diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py
index e7ea39ea918..8c43f7d7ab7 100644
--- a/Tests/test_file_mic.py
+++ b/Tests/test_file_mic.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImagePalette
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index d4eb0254bc5..650c65b5d4e 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
from io import BytesIO
diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py
index f4e357ae0fd..9037ea33b54 100644
--- a/Tests/test_file_msp.py
+++ b/Tests/test_file_msp.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import pytest
diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py
index 735840de4cb..eba69415395 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os.path
import subprocess
diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py
index 596a3414f79..1a37c6ab31d 100644
--- a/Tests/test_file_pcd.py
+++ b/Tests/test_file_pcd.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index f42ec4a6894..2565e0b6ddc 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageFile, PcxImagePlugin
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index eba23798d6a..ceb0f0ca133 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import os.path
import tempfile
diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py
index 63779f202cb..c6ddc54e714 100644
--- a/Tests/test_file_pixar.py
+++ b/Tests/test_file_pixar.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, PixarImagePlugin
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index ff386211061..ae2a4772b60 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import re
import sys
import warnings
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index bb49a46d376..32de42ed45d 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
from io import BytesIO
@@ -6,7 +7,12 @@
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 +90,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, 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"))
- assert_image_equal_tofile(im, 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_file_psd.py b/Tests/test_file_psd.py
index 8b06ce2b163..16f049602bc 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py
index b7c9457294d..6dc468754ef 100644
--- a/Tests/test_file_qoi.py
+++ b/Tests/test_file_qoi.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, QoiImagePlugin
diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py
index 13698276b8c..bc45bbfd34d 100644
--- a/Tests/test_file_sgi.py
+++ b/Tests/test_file_sgi.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, SgiImagePlugin
diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py
index f2109875478..42d833fb25c 100644
--- a/Tests/test_file_spider.py
+++ b/Tests/test_file_spider.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import tempfile
import warnings
from io import BytesIO
diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py
index 874b37b5285..41f3b7d98f3 100644
--- a/Tests/test_file_sun.py
+++ b/Tests/test_file_sun.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import pytest
diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py
index 4470823cdbd..58226c33062 100644
--- a/Tests/test_file_tar.py
+++ b/Tests/test_file_tar.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index d0f228573bb..eafb61d30af 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
from glob import glob
from itertools import product
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 46acb366285..66e17d67ec3 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import warnings
from io import BytesIO
@@ -484,13 +485,13 @@ def test_modify_exif(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
- exif[256] = 100
+ exif[264] = 100
im.save(outfile, exif=exif)
with Image.open(outfile) as im:
exif = im.getexif()
- assert exif[256] == 100
+ assert exif[264] == 100
def test_reload_exif_after_seek(self):
with Image.open("Tests/images/multipage.tiff") as im:
@@ -612,6 +613,14 @@ def test_roundtrip_tiff_uint16(self, tmp_path):
assert_image_equal_tofile(im, tmpfile)
+ def test_rowsperstrip(self, tmp_path):
+ outfile = str(tmp_path / "temp.tif")
+ im = hopper()
+ im.save(outfile, tiffinfo={278: 256})
+
+ with Image.open(outfile) as im:
+ assert im.tag_v2[278] == 256
+
def test_strip_raw(self):
infile = "Tests/images/tiff_strip_raw.tif"
with Image.open(infile) as im:
@@ -818,6 +827,27 @@ def test_get_photoshop_blocks(self):
4001,
]
+ def test_tiff_chunks(self, tmp_path):
+ tmpfile = str(tmp_path / "temp.tif")
+
+ im = hopper()
+ with open(tmpfile, "wb") as fp:
+ for y in range(0, 128, 32):
+ chunk = im.crop((0, y, 128, y + 32))
+ if y == 0:
+ chunk.save(
+ fp,
+ "TIFF",
+ tiffinfo={
+ TiffImagePlugin.IMAGEWIDTH: 128,
+ TiffImagePlugin.IMAGELENGTH: 128,
+ },
+ )
+ else:
+ fp.write(chunk.tobytes())
+
+ assert_image_equal_tofile(im, tmpfile)
+
def test_close_on_load_exclusive(self, tmp_path):
# similar to test_fd_leak, but runs on unixlike os
tmpfile = str(tmp_path / "temp.tif")
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index edd57e6b54d..06689bc9092 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import struct
@@ -123,6 +124,7 @@ def test_write_metadata(tmp_path):
"""Test metadata writing through the python code"""
with Image.open("Tests/images/hopper.tif") as img:
f = str(tmp_path / "temp.tiff")
+ del img.tag[278]
img.save(f, tiffinfo=img.tag)
original = img.tag_v2.named()
@@ -159,9 +161,11 @@ def test_change_stripbytecounts_tag_type(tmp_path):
out = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper.tif") as im:
info = im.tag_v2
+ del info[278]
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
im = im.resize((500, 500))
+ info[TiffImagePlugin.IMAGEWIDTH] = im.width
# STRIPBYTECOUNTS can be a SHORT or a LONG
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py
index 0b84d0320aa..7acec975942 100644
--- a/Tests/test_file_wal.py
+++ b/Tests/test_file_wal.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import WalImageFile
from .helper import assert_image_equal_tofile
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index ba41a1162fd..cb6ef47d2be 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import re
import sys
import warnings
diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py
index 79d01a4446a..cfda35a0962 100644
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py
index 22acb4be68f..426fe7a0293 100644
--- a/Tests/test_file_webp_animated.py
+++ b/Tests/test_file_webp_animated.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from packaging.version import parse as parse_version
diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py
index 6acf58ac3f5..08c80973a74 100644
--- a/Tests/test_file_webp_lossless.py
+++ b/Tests/test_file_webp_lossless.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index a7b7bbcf6ee..deaf5e380db 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py
index 596dc8ba181..6e1d4c1361f 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, WmfImagePlugin
diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py
index b086ffd683f..69a0a1b38d8 100644
--- a/Tests/test_file_xbm.py
+++ b/Tests/test_file_xbm.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py
index 265feab4294..529a4558073 100644
--- a/Tests/test_file_xpm.py
+++ b/Tests/test_file_xpm.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, XpmImagePlugin
diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py
index 5848995c1aa..b87494eba18 100644
--- a/Tests/test_file_xvthumb.py
+++ b/Tests/test_file_xvthumb.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, XVThumbImagePlugin
diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py
index 1e5eff2f15d..e5e85618651 100644
--- a/Tests/test_font_bdf.py
+++ b/Tests/test_font_bdf.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import BdfFontFile, FontFile
diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py
index 388ee711861..e3c72c1aebd 100644
--- a/Tests/test_font_crash.py
+++ b/Tests/test_font_crash.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageFont
diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py
index 6a038bb4038..4e29a856bc9 100644
--- a/Tests/test_font_leaks.py
+++ b/Tests/test_font_leaks.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image, ImageDraw, ImageFont
from .helper import PillowLeakTestCase, skip_unless_feature
diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py
index 4365b931066..e6abede0787 100644
--- a/Tests/test_font_pcf.py
+++ b/Tests/test_font_pcf.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import pytest
diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py
index 950e5029ff5..4c2d7185e3c 100644
--- a/Tests/test_font_pcf_charsets.py
+++ b/Tests/test_font_pcf_charsets.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import pytest
diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py
new file mode 100644
index 00000000000..eda8fb81283
--- /dev/null
+++ b/Tests/test_fontfile.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+import pytest
+
+from PIL import FontFile
+
+
+def test_save(tmp_path):
+ tempname = str(tmp_path / "temp.pil")
+
+ font = FontFile.FontFile()
+ with pytest.raises(ValueError):
+ font.save(tempname)
diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py
index fd47fae39ba..6395ae4aad8 100644
--- a/Tests/test_format_hsv.py
+++ b/Tests/test_format_hsv.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import colorsys
import itertools
diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py
index c7610ce8a6c..a55620e09ab 100644
--- a/Tests/test_format_lab.py
+++ b/Tests/test_format_lab.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 615e00e40da..dd989ad99b5 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import io
import logging
import os
@@ -1016,6 +1017,11 @@ def test_fli_overrun2(self):
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
+ def test_exit_fp(self):
+ with Image.new("L", (1, 1)) as im:
+ pass
+ assert not hasattr(im, "fp")
+
def test_close_graceful(self, caplog):
with Image.open("Tests/images/hopper.jpg") as im:
copy = im.copy()
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 4a794371d67..4ae56fae0cc 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import subprocess
import sys
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index b3e5d9e3e08..0dacb3157a2 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from packaging.version import parse as parse_version
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 7c17040d30b..d4ddc2a31b2 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py
index abf5f846f89..3a26ef96e74 100644
--- a/Tests/test_image_copy.py
+++ b/Tests/test_image_copy.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import copy
import pytest
diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py
index 0bb54e5d811..5e02a3b0dfc 100644
--- a/Tests/test_image_crop.py
+++ b/Tests/test_image_crop.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py
index 774272dd1f7..08c40af1f16 100644
--- a/Tests/test_image_draft.py
+++ b/Tests/test_image_draft.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
from .helper import fromstring, skip_unless_feature, tostring
diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py
index 031fceda3fa..fce16122409 100644
--- a/Tests/test_image_entropy.py
+++ b/Tests/test_image_entropy.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from .helper import hopper
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index 5bd7ee0d273..3fa5dd2423a 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py
index 017da499d2c..5d5e9f2dfc9 100644
--- a/Tests/test_image_frombytes.py
+++ b/Tests/test_image_frombytes.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py
index b3ca43bde6d..76b576da56b 100644
--- a/Tests/test_image_fromqimage.py
+++ b/Tests/test_image_fromqimage.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py
index e7701dbc460..64339e2cd85 100644
--- a/Tests/test_image_getbands.py
+++ b/Tests/test_image_getbands.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py
index 9e792cfdf7c..b18a7202e38 100644
--- a/Tests/test_image_getbbox.py
+++ b/Tests/test_image_getbbox.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py
index dea3a60a112..17460fa93f5 100644
--- a/Tests/test_image_getcolors.py
+++ b/Tests/test_image_getcolors.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from .helper import hopper
diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py
index 873cc65bf91..ace64279b8c 100644
--- a/Tests/test_image_getdata.py
+++ b/Tests/test_image_getdata.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
from .helper import hopper
diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py
index b17c8a786dd..6bbc4da9a01 100644
--- a/Tests/test_image_getextrema.py
+++ b/Tests/test_image_getextrema.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
from .helper import hopper
diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py
index e969c8164a2..bc8a7485eb0 100644
--- a/Tests/test_image_getim.py
+++ b/Tests/test_image_getim.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from .helper import hopper
diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py
index a5be972d3ed..4340f46f619 100644
--- a/Tests/test_image_getpalette.py
+++ b/Tests/test_image_getpalette.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
from .helper import hopper
diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py
index aa47be3b2e0..e90f5f5056f 100644
--- a/Tests/test_image_getprojection.py
+++ b/Tests/test_image_getprojection.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
from .helper import hopper
diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py
index 7ba2f10b765..3ac6649e074 100644
--- a/Tests/test_image_histogram.py
+++ b/Tests/test_image_histogram.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from .helper import hopper
diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py
index 17847c4fd9c..36f8ba575d8 100644
--- a/Tests/test_image_load.py
+++ b/Tests/test_image_load.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import logging
import os
diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py
index ad90d1250dc..3c1d494fab2 100644
--- a/Tests/test_image_mode.py
+++ b/Tests/test_image_mode.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageMode
diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py
index 0b87f607286..fd117f9dbc9 100644
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py
index fce45ec4f0e..2232b94429d 100644
--- a/Tests/test_image_point.py
+++ b/Tests/test_image_point.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from .helper import assert_image_equal, hopper
diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py
index 0ba7e5919a5..c44b048d523 100644
--- a/Tests/test_image_putalpha.py
+++ b/Tests/test_image_putalpha.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index d3cb13e2e97..2648af8fa2e 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
from array import array
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
index de2d9024213..43b65be2b17 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImagePalette
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
index 54c567aae6c..1475b027bd8 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from packaging.version import parse as parse_version
diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py
index a4d0f510761..ba9100415a2 100644
--- a/Tests/test_image_reduce.py
+++ b/Tests/test_image_reduce.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageMath, ImageMode
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index b4bf6c8df3a..af730dce13d 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from contextlib import contextmanager
import pytest
@@ -403,7 +404,7 @@ def test_reduce(self):
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_resize.py b/Tests/test_image_resize.py
index 0d3b43ee29f..aedcf4a09b5 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -2,6 +2,7 @@
Tests for resize functionality.
"""
from __future__ import annotations
+
from itertools import permutations
import pytest
diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py
index 0931aa32d98..e63fef2c152 100644
--- a/Tests/test_image_rotate.py
+++ b/Tests/test_image_rotate.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py
index 707508250f6..c39a100e777 100644
--- a/Tests/test_image_split.py
+++ b/Tests/test_image_split.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, features
diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py
index 9e6796ca299..7fa5692aa7d 100644
--- a/Tests/test_image_thumbnail.py
+++ b/Tests/test_image_thumbnail.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py
index 156b9919d27..89a41cf8ec2 100644
--- a/Tests/test_image_tobitmap.py
+++ b/Tests/test_image_tobitmap.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from .helper import assert_image_equal, fromstring, hopper
diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py
index f6042bca527..8f15adac065 100644
--- a/Tests/test_image_tobytes.py
+++ b/Tests/test_image_tobytes.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from .helper import hopper
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index 15939ef647c..0fe9fd1d5f5 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import math
import pytest
@@ -10,18 +11,25 @@
class TestImageTransform:
def test_sanity(self):
- im = Image.new("L", (100, 100))
-
- 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)
+ im = hopper()
+
+ 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_image_transpose.py b/Tests/test_image_transpose.py
index 66a2d9e2955..01bf5a83918 100644
--- a/Tests/test_image_transpose.py
+++ b/Tests/test_image_transpose.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL.Image import Transpose
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index 8e3a738d708..2f0614385aa 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from PIL import Image, ImageChops
from .helper import assert_image_equal, hopper
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 0dde82bd748..03332699a1d 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import datetime
import os
import re
@@ -49,8 +50,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 +91,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 +638,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/Tests/test_imagecolor.py b/Tests/test_imagecolor.py
index c0ffd2ebf0b..b602172b642 100644
--- a/Tests/test_imagecolor.py
+++ b/Tests/test_imagecolor.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageColor
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 379fe78cd8a..69aab48912b 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import contextlib
import os.path
diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py
index d729af14d3a..004c2d768fd 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os.path
import pytest
diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py
index f4e4d59be32..e3d8a7ab251 100644
--- a/Tests/test_imageenhance.py
+++ b/Tests/test_imageenhance.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageEnhance
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index 4804a554f83..99731f35208 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
import pytest
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 6e04cddc748..d2c87d42aaa 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import copy
import os
import re
@@ -1053,11 +1054,13 @@ def test_too_many_characters(font):
with pytest.raises(ValueError):
transposed_font.getlength("A" * 1_000_001)
- default_font = ImageFont.load_default()
+ imagefont = ImageFont.ImageFont()
+ with pytest.raises(ValueError):
+ imagefont.getlength("A" * 1_000_001)
with pytest.raises(ValueError):
- default_font.getlength("A" * 1_000_001)
+ imagefont.getbbox("A" * 1_000_001)
with pytest.raises(ValueError):
- default_font.getbbox("A" * 1_000_001)
+ imagefont.getmask("A" * 1_000_001)
@pytest.mark.parametrize(
diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py
index bea532b051e..09e68ea488a 100644
--- a/Tests/test_imagefontctl.py
+++ b/Tests/test_imagefontctl.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageFont
diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py
index 21b4dee3c4a..be4be1c546e 100644
--- a/Tests/test_imagefontpil.py
+++ b/Tests/test_imagefontpil.py
@@ -1,14 +1,24 @@
from __future__ import annotations
+
+import struct
+from io import BytesIO
+
import pytest
-from PIL import Image, ImageDraw, ImageFont, features
+from PIL import Image, ImageDraw, ImageFont, _util, features
from .helper import assert_image_equal_tofile
-pytestmark = pytest.mark.skipif(
- features.check_module("freetype2"),
- reason="PILfont superseded if FreeType is supported",
-)
+original_core = ImageFont.core
+
+
+def setup_module():
+ if features.check_module("freetype2"):
+ ImageFont.core = _util.DeferredError(ImportError)
+
+
+def teardown_module():
+ ImageFont.core = original_core
def test_default_font():
@@ -44,3 +54,25 @@ def test_textbbox():
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
+
+
+def test_decompression_bomb():
+ glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256)
+ fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
+
+ font = ImageFont.ImageFont()
+ font._load_pilfont_data(fp, Image.new("L", (256, 256)))
+ with pytest.raises(Image.DecompressionBombError):
+ font.getmask("A" * 1_000_000)
+
+
+@pytest.mark.timeout(4)
+def test_oom():
+ glyph = struct.pack(
+ ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767
+ )
+ fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256)
+
+ font = ImageFont.ImageFont()
+ font._load_pilfont_data(fp, Image.new("L", (1, 1)))
+ font.getmask("A" * 1_000_000)
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index b7683ec1817..9d3d40398f6 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import shutil
import subprocess
diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py
index 22de86c7cab..622ad27eacb 100644
--- a/Tests/test_imagemath.py
+++ b/Tests/test_imagemath.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageMath
@@ -64,6 +65,16 @@ def test_prevent_exec(expression):
ImageMath.eval(expression)
+def test_prevent_double_underscores():
+ with pytest.raises(ValueError):
+ ImageMath.eval("1", {"__": None})
+
+
+def test_prevent_builtins():
+ with pytest.raises(ValueError):
+ ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None})
+
+
def test_logical():
assert pixel(ImageMath.eval("not A", images)) == 0
assert pixel(ImageMath.eval("A and B", images)) == "L 2"
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index ec55aadf9b3..0708ee63905 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -1,5 +1,6 @@
# Test the ImageMorphology functionality
from __future__ import annotations
+
import pytest
from PIL import Image, ImageMorph, _imagingmorph
@@ -57,15 +58,6 @@ def test_str_to_img():
assert_image_equal_tofile(A, "Tests/images/morph_a.png")
-def create_lut():
- for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"):
- lb = ImageMorph.LutBuilder(op_name=op)
- lut = lb.build_lut()
- with open(f"Tests/images/{op}.lut", "wb") as f:
- f.write(lut)
-
-
-# create_lut()
@pytest.mark.parametrize(
"op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
)
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 7980bead0a4..636b99dbe8f 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageDraw, ImageOps, ImageStat, features
diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py
index 84d3a69507a..8ffb9bff7a2 100644
--- a/Tests/test_imageops_usm.py
+++ b/Tests/test_imageops_usm.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageFilter
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index e5b59b74a84..be21464b4ae 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImagePalette
diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py
index ac3ea3281f5..5c6393e237f 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import array
import math
import struct
diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py
index 41d247f429f..d55d980d9be 100644
--- a/Tests/test_imageqt.py
+++ b/Tests/test_imageqt.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py
index 6d71e4d87af..66d553bcbc7 100644
--- a/Tests/test_imagesequence.py
+++ b/Tests/test_imagesequence.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageSequence, TiffImagePlugin
diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py
index 761d28d3019..0996ad41d4d 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageShow
diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py
index 7b56b89cc4b..01687db353e 100644
--- a/Tests/test_imagestat.py
+++ b/Tests/test_imagestat.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image, ImageStat
diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py
index bb20fbb6f92..c06fc58235b 100644
--- a/Tests/test_imagetk.py
+++ b/Tests/test_imagetk.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py
index 6927eedcf86..f93eabcb4ff 100644
--- a/Tests/test_imagewin.py
+++ b/Tests/test_imagewin.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import ImageWin
diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py
index bd154335af7..63d6b903ca6 100644
--- a/Tests/test_imagewin_pointers.py
+++ b/Tests/test_imagewin_pointers.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
from PIL import Image, ImageWin
diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py
index 92cad4ac1b1..1c642e4c981 100644
--- a/Tests/test_lib_image.py
+++ b/Tests/test_lib_image.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py
index 1293f7628be..e2024abbf2e 100644
--- a/Tests/test_lib_pack.py
+++ b/Tests/test_lib_pack.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
import pytest
diff --git a/Tests/test_locale.py b/Tests/test_locale.py
index 49b052fa485..db9557d7ba2 100644
--- a/Tests/test_locale.py
+++ b/Tests/test_locale.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import locale
import pytest
diff --git a/Tests/test_main.py b/Tests/test_main.py
index a84e61a7b7d..9f61a0c8169 100644
--- a/Tests/test_main.py
+++ b/Tests/test_main.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import subprocess
import sys
diff --git a/Tests/test_map.py b/Tests/test_map.py
index 76444f33d1c..9c79fe35906 100644
--- a/Tests/test_map.py
+++ b/Tests/test_map.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import sys
import pytest
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index 3e17d8dccde..d3ee511b7c2 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 6f0e99b3f93..24dff36a610 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import warnings
import pytest
diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py
index aeeafb6f1df..a89d75b59d3 100644
--- a/Tests/test_pdfparser.py
+++ b/Tests/test_pdfparser.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import time
import pytest
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
index eb687b57b62..c445e349447 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pickle
import pytest
diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py
index 77c7952e912..7f618d0f53b 100644
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import os
import sys
from io import BytesIO
diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py
index 08133b6c30c..c2cea08ca61 100644
--- a/Tests/test_pyroma.py
+++ b/Tests/test_pyroma.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import __version__
diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py
index 49ca016771f..ad2b5ad9bf5 100644
--- a/Tests/test_qt_image_qapplication.py
+++ b/Tests/test_qt_image_qapplication.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import ImageQt
diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py
index 396bd9080ca..b26787ce668 100644
--- a/Tests/test_qt_image_toqimage.py
+++ b/Tests/test_qt_image_toqimage.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import ImageQt
diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py
index 37d72d451de..dee6258ecb5 100644
--- a/Tests/test_sgi_crash.py
+++ b/Tests/test_sgi_crash.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import Image
diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py
index d93b0390416..9f3e86a32a8 100644
--- a/Tests/test_shell_injection.py
+++ b/Tests/test_shell_injection.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import shutil
import pytest
diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py
index e7b41fb4738..c07e7f7d395 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from fractions import Fraction
from PIL import Image, TiffImagePlugin, features
diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py
index 6b693f7cd5d..75326288f97 100644
--- a/Tests/test_uploader.py
+++ b/Tests/test_uploader.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from .helper import assert_image_equal, assert_image_similar, hopper
diff --git a/Tests/test_util.py b/Tests/test_util.py
index 1457d85f795..3395ef753d7 100644
--- a/Tests/test_util.py
+++ b/Tests/test_util.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
import pytest
from PIL import _util
@@ -66,7 +67,7 @@ def test_deferred_error():
# Arrange
# Act
- thing = _util.DeferredError(ValueError("Some error text"))
+ thing = _util.DeferredError.new(ValueError("Some error text"))
# Assert
with pytest.raises(ValueError):
diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py
index 28ebc7d79f1..0f51abc9574 100644
--- a/Tests/test_webp_leaks.py
+++ b/Tests/test_webp_leaks.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+
from io import BytesIO
from PIL import Image
diff --git a/docs/COPYING b/docs/COPYING
index bc44ba388a6..73af6d99c0f 100644
--- a/docs/COPYING
+++ b/docs/COPYING
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors
+ Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors
Like PIL, Pillow is licensed under the open source PIL
Software License:
diff --git a/docs/PIL.rst b/docs/PIL.rst
index fa036b9ccfe..bdbf1373d88 100644
--- a/docs/PIL.rst
+++ b/docs/PIL.rst
@@ -69,10 +69,10 @@ can be found here.
:undoc-members:
:show-inheritance:
-:mod:`~PIL.ImageTransform` Module
----------------------------------
+:mod:`~PIL.ImageMode` Module
+----------------------------
-.. automodule:: PIL.ImageTransform
+.. automodule:: PIL.ImageMode
:members:
:undoc-members:
:show-inheritance:
diff --git a/docs/about.rst b/docs/about.rst
index 872ac0ea690..cdb32ca5d10 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/conf.py b/docs/conf.py
index a70dece7469..9ae7ae605fd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -54,7 +54,7 @@
# General information about the project.
project = "Pillow (PIL Fork)"
copyright = (
- "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors"
+ "1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors"
)
author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 75c0b73ebed..205fcb9abcd 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -44,6 +44,54 @@ ImageFile.raise_oserror
error codes returned by a codec's ``decode()`` method, which ImageFile already does
automatically.
+IptcImageFile helper functions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 10.2.0
+
+The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
+``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). These are undocumented helper functions intended
+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
----------------
@@ -107,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::
@@ -327,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 9cd65fd48cb..569ccb7691b 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -487,6 +487,16 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exif**
If present, the image will be stored with the provided raw EXIF data.
+**keep_rgb**
+ By default, libjpeg converts images with an RGB color space to YCbCr.
+ If this option is present and true, those images will be stored as RGB
+ instead.
+
+ When this option is enabled, attempting to chroma-subsample RGB images
+ with the ``subsampling`` option will raise an :py:exc:`OSError`.
+
+ .. versionadded:: 10.2.0
+
**subsampling**
If present, sets the subsampling for the encoder.
@@ -552,12 +562,13 @@ JPEG 2000
.. versionadded:: 2.4.0
-Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or
-``RGBA`` data. It can also read files containing ``YCbCr`` data, which it
-converts on read into ``RGB`` or ``RGBA`` depending on whether or not there is
-an alpha channel. Pillow supports JPEG 2000 raw codestreams (``.j2k`` files),
-as well as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files). Pillow does
-*not* support files whose components have different sampling frequencies.
+Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
+``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
+``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
+Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
+``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports
+JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
+(``.jp2`` or ``.jpx`` files).
When loading, if you set the ``mode`` on the image prior to the
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
@@ -685,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/handbook/tutorial.rst b/docs/handbook/tutorial.rst
index d79f2465f16..523e2ad7494 100644
--- a/docs/handbook/tutorial.rst
+++ b/docs/handbook/tutorial.rst
@@ -542,7 +542,7 @@ Reading from URL
from PIL import Image
from urllib.request import urlopen
- url = "https://python-pillow.org/images/pillow-logo.png"
+ url = "https://python-pillow.org/assets/images/pillow-logo.png"
img = Image.open(urlopen(url))
diff --git a/docs/index.rst b/docs/index.rst
index 4f577fe9c22..5583699193c 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 464ab77ea35..06965ead3f0 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 9b9b5e7b29e..c4484cbe22a 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/ImageGrab.rst b/docs/reference/ImageGrab.rst
index 0b94032d5f8..db2987eb081 100644
--- a/docs/reference/ImageGrab.rst
+++ b/docs/reference/ImageGrab.rst
@@ -11,9 +11,9 @@ or the clipboard to a PIL image memory.
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None)
- Take a snapshot of the screen. The pixels inside the bounding box are
- returned as an "RGBA" on macOS, or an "RGB" image otherwise.
- If the bounding box is omitted, the entire screen is copied.
+ Take a snapshot of the screen. The pixels inside the bounding box are returned as
+ an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted,
+ the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen.
On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return
a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is
@@ -22,7 +22,10 @@ or the clipboard to a PIL image memory.
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
:param bbox: What region to copy. Default is the entire screen.
- Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used.
+ On macOS, this is not increased to 2x for Retina screens, so the full
+ width of a Retina screen would be 1440, not 2880.
+ On Windows, the top-left point may be negative if ``all_screens=True``
+ is used.
:param include_layered_windows: Includes layered windows. Windows OS only.
.. versionadded:: 6.1.0
diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst
new file mode 100644
index 00000000000..5b0a5ce49dc
--- /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 5d6affa94ad..82c75e373ad 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/reference/internal_modules.rst b/docs/reference/internal_modules.rst
index 363a67d9b02..f2932c32200 100644
--- a/docs/reference/internal_modules.rst
+++ b/docs/reference/internal_modules.rst
@@ -25,6 +25,19 @@ Internal Modules
:undoc-members:
:show-inheritance:
+:mod:`~PIL._typing` Module
+--------------------------
+
+.. module:: PIL._typing
+
+Provides a convenient way to import type hints that are not available
+on some Python versions.
+
+.. py:data:: TypeGuard
+ :value: typing.TypeGuard
+
+ See :py:obj:`typing.TypeGuard`.
+
:mod:`~PIL._util` Module
------------------------
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index a3f238119f0..705ca04152f 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.2.0.rst b/docs/releasenotes/10.2.0.rst
index 9883f10baf3..c3947f64c2e 100644
--- a/docs/releasenotes/10.2.0.rst
+++ b/docs/releasenotes/10.2.0.rst
@@ -1,14 +1,6 @@
10.2.0
------
-Backwards Incompatible Changes
-==============================
-
-TODO
-^^^^
-
-TODO
-
Deprecations
============
@@ -20,10 +12,14 @@ ImageFile.raise_oserror
error codes returned by a codec's ``decode()`` method, which ImageFile already does
automatically.
-TODO
-^^^^
+IptcImageFile helper functions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
+The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
+``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow
+12.0.0 (2025-10-15). These are undocumented helper functions intended
+for internal use, so there is no replacement. They can each be replaced
+by a single line of code using builtin functions in Python.
API Changes
===========
@@ -46,6 +42,14 @@ Added DdsImagePlugin enums
:py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT`
enums have been added to :py:class:`PIL.DdsImagePlugin`.
+JPEG RGB color space
+^^^^^^^^^^^^^^^^^^^^
+
+When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB
+images in the RGB color space instead of being converted to YCbCr automatically by
+libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with
+the ``subsampling`` option will raise an :py:exc:`OSError`.
+
JPEG restart marker interval
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -62,10 +66,34 @@ output only the quantization and Huffman tables for the image.
Security
========
-TODO
-^^^^
+ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To protect against potential DOS attacks when using arbitrary strings as text input,
+Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into
+:py:meth:`PIL.ImageFont.ImageFont.getmask` is over a certain limit,
+:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.
+
+This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It
+can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.
-TODO
+A decompression bomb check has also been added to
+:py:meth:`PIL.ImageFont.ImageFont.getmask`.
+
+ImageFont.getmask: Trim glyph size
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To protect against potential DOS attacks when using PIL fonts,
+:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that
+they do not extend beyond the bitmap image.
+
+ImageMath.eval: Restricted environment keys
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:cve:`2023-50447`: If an attacker has control over the keys passed to the
+``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute
+arbitrary code. To prevent this, keys matching the names of builtins and keys
+containing double underscores will now raise a :py:exc:`ValueError`.
Other Changes
=============
@@ -78,16 +106,56 @@ Support has been added to read the BC4U format of DDS images.
Support has also been added to read DX10 BC1 and BC4, whether UNORM or
TYPELESS.
+Support arbitrary masks for uncompressed RGB DDS images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+All masks are now supported when reading DDS images with uncompressed RGB data,
+allowing for bit counts other than 24 and 32.
+
+Saving TIFF tag RowsPerStrip
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by
+the user, rather than always being calculated by Pillow.
+
+Optimized ImageColor.getrgb and getcolor
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and
+:py:attr:`~PIL.ImageColor.getcolor` are now cached using
+:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times
+as fast and ``getcolor`` are 5.1 - 19.6 times as fast.
+
+Optimized ImageMode.getmode
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using
+:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as
+fast.
+
Optimized ImageStat.Stat count and extrema
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and
:py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the
-histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3x as fast
-on average and ``st.extrema`` is 12x as fast on average.
+histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on
+average and ``st.extrema`` is 12 times as fast on average.
Encoder errors now report error detail as string
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:exc:`OSError` exceptions from image encoders now include a textual description of
the error instead of a numeric error code.
+
+Type hints
+^^^^^^^^^^
+
+Work has begun to add type annotations to Pillow, including:
+
+* :py:mod:`~PIL.ContainerIO`
+* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile`
+* :py:mod:`~PIL.ImageChops`
+* :py:mod:`~PIL.ImageMode`
+* :py:mod:`~PIL.ImageSequence`
+* :py:mod:`~PIL.ImageTransform`
+* :py:mod:`~PIL.TarIO`
diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst
new file mode 100644
index 00000000000..8772a382dc2
--- /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 2bf299dd3d8..1fc245c9a3c 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 02da702a799..6400218f467 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 fde2faae3a7..16075ce95ec 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 d8034853cc2..e86f8082b48 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 193e8c9b247..b1ce9cf1d07 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,6 +65,9 @@ tests = [
"pytest-cov",
"pytest-timeout",
]
+typing = [
+ 'typing-extensions; python_version < "3.10"',
+]
xmp = [
"defusedxml",
]
@@ -116,7 +119,8 @@ extend-ignore = [
]
[tool.ruff.per-file-ignores]
-"Tests/*.py" = ["I001"]
+"Tests/oss-fuzz/fuzz_font.py" = ["I002"]
+"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
[tool.ruff.isort]
known-first-party = ["PIL"]
@@ -141,13 +145,7 @@ exclude = [
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
- '^src/PIL/ImageCms.py$',
- '^src/PIL/ImageFile.py$',
- '^src/PIL/ImageFont.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/BdfFontFile.py b/src/PIL/BdfFontFile.py
index b12ddc2d4b4..e3eda4fe98c 100644
--- a/src/PIL/BdfFontFile.py
+++ b/src/PIL/BdfFontFile.py
@@ -22,6 +22,8 @@
"""
from __future__ import annotations
+from typing import BinaryIO
+
from . import FontFile, Image
bdf_slant = {
@@ -36,7 +38,17 @@
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
-def bdf_char(f):
+def bdf_char(
+ f: BinaryIO,
+) -> (
+ tuple[
+ str,
+ int,
+ tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
+ Image.Image,
+ ]
+ | None
+):
# skip to STARTCHAR
while True:
s = f.readline()
@@ -56,13 +68,12 @@ def bdf_char(f):
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
# load bitmap
- bitmap = []
+ bitmap = bytearray()
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
break
- bitmap.append(s[:-1])
- bitmap = b"".join(bitmap)
+ bitmap += s[:-1]
# The word BBX
# followed by the width in x (BBw), height in y (BBh),
@@ -92,7 +103,7 @@ def bdf_char(f):
class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format."""
- def __init__(self, fp):
+ def __init__(self, fp: BinaryIO):
super().__init__()
s = fp.readline()
diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py
index 64d04242639..0035296a45c 100644
--- a/src/PIL/ContainerIO.py
+++ b/src/PIL/ContainerIO.py
@@ -16,15 +16,16 @@
from __future__ import annotations
import io
+from typing import IO, AnyStr, Generic, Literal
-class ContainerIO:
+class ContainerIO(Generic[AnyStr]):
"""
A file object that provides read access to a part of an existing
file (for example a TAR file).
"""
- def __init__(self, file, offset, length) -> None:
+ def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None:
"""
Create file object.
@@ -32,7 +33,7 @@ def __init__(self, file, offset, length) -> None:
:param offset: Start of region, in bytes.
:param length: Size of region, in bytes.
"""
- self.fh = file
+ self.fh: IO[AnyStr] = file
self.pos = 0
self.offset = offset
self.length = length
@@ -41,10 +42,10 @@ def __init__(self, file, offset, length) -> None:
##
# Always false.
- def isatty(self):
+ def isatty(self) -> bool:
return False
- def seek(self, offset, mode=io.SEEK_SET):
+ def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None:
"""
Move file pointer.
@@ -63,7 +64,7 @@ def seek(self, offset, mode=io.SEEK_SET):
self.pos = max(0, min(self.pos, self.length))
self.fh.seek(self.offset + self.pos)
- def tell(self):
+ def tell(self) -> int:
"""
Get current file pointer.
@@ -71,7 +72,7 @@ def tell(self):
"""
return self.pos
- def read(self, n=0):
+ def read(self, n: int = 0) -> AnyStr:
"""
Read data.
@@ -84,17 +85,17 @@ def read(self, n=0):
else:
n = self.length - self.pos
if not n: # EOF
- return b"" if "b" in self.fh.mode else ""
+ return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
self.pos = self.pos + n
return self.fh.read(n)
- def readline(self):
+ def readline(self) -> AnyStr:
"""
Read a line of text.
:returns: An 8-bit string.
"""
- s = b"" if "b" in self.fh.mode else ""
+ s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
newline_character = b"\n" if "b" in self.fh.mode else "\n"
while True:
c = self.read(1)
@@ -105,7 +106,7 @@ def readline(self):
break
return s
- def readlines(self):
+ def readlines(self) -> list[AnyStr]:
"""
Read multiple lines of text.
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index 5b6ac2ead50..eb4c8f557af 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -18,6 +18,7 @@
from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32
+from ._binary import o8
from ._binary import o32le as o32
# Magic ("DDS ")
@@ -341,6 +342,7 @@ def _open(self):
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
+ extents = (0, 0) + self.size
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
@@ -351,22 +353,16 @@ def _open(self):
rawmode = None
if pfflags & DDPF.RGB:
# Texture contains uncompressed RGB data
- masks = struct.unpack("<4I", header.read(16))
- masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
- if bitcount == 24:
- self._mode = "RGB"
- rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000]
- elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS:
+ if pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA"
- rawmode = (
- masks[0x000000FF]
- + masks[0x0000FF00]
- + masks[0x00FF0000]
- + masks[0xFF000000]
- )
+ mask_count = 4
else:
- msg = f"Unsupported bitcount {bitcount} for {pfflags}"
- raise OSError(msg)
+ self._mode = "RGB"
+ mask_count = 3
+
+ masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
+ self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
+ return
elif pfflags & DDPF.LUMINANCE:
if bitcount == 8:
self._mode = "L"
@@ -464,7 +460,6 @@ def _open(self):
msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg)
- extents = (0, 0) + self.size
if n:
self.tile = [
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
@@ -476,6 +471,39 @@ def load_seek(self, pos):
pass
+class DdsRgbDecoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+
+ def decode(self, buffer):
+ bitcount, masks = self.args
+
+ # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
+ # Calculate how many zeros each mask is padded with
+ mask_offsets = []
+ # And the maximum value of each channel without the padding
+ mask_totals = []
+ for mask in masks:
+ offset = 0
+ if mask != 0:
+ while mask >> (offset + 1) << (offset + 1) == mask:
+ offset += 1
+ mask_offsets.append(offset)
+ mask_totals.append(mask >> offset)
+
+ data = bytearray()
+ bytecount = bitcount // 8
+ while len(data) < self.state.xsize * self.state.ysize * len(masks):
+ value = int.from_bytes(self.fd.read(bytecount), "little")
+ for i, mask in enumerate(masks):
+ masked_value = value & mask
+ # Remove the zero padding, and scale it to 8 bits
+ data += o8(
+ int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
+ )
+ self.set_as_raw(bytes(data))
+ return -1, 0
+
+
def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS"
@@ -533,5 +561,6 @@ def _accept(prefix):
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
+Image.register_decoder("dds_rgb", DdsRgbDecoder)
Image.register_save(DdsImageFile.format, _save)
Image.register_extension(DdsImageFile.format, ".dds")
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index 7dce2d60f76..e69890babcc 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -15,7 +15,7 @@
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/FontFile.py b/src/PIL/FontFile.py
index 9621770e2a9..3ec1ae819fc 100644
--- a/src/PIL/FontFile.py
+++ b/src/PIL/FontFile.py
@@ -16,13 +16,16 @@
from __future__ import annotations
import os
+from typing import BinaryIO
from . import Image, _binary
WIDTH = 800
-def puti16(fp, values):
+def puti16(
+ fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int]
+) -> None:
"""Write network order (big-endian) 16-bit sequence"""
for v in values:
if v < 0:
@@ -33,16 +36,34 @@ def puti16(fp, values):
class FontFile:
"""Base class for raster font file handlers."""
- bitmap = None
-
- def __init__(self):
- self.info = {}
- self.glyph = [None] * 256
-
- def __getitem__(self, ix):
+ bitmap: Image.Image | None = None
+
+ def __init__(self) -> None:
+ self.info: dict[bytes, bytes | int] = {}
+ self.glyph: list[
+ tuple[
+ tuple[int, int],
+ tuple[int, int, int, int],
+ tuple[int, int, int, int],
+ Image.Image,
+ ]
+ | None
+ ] = [None] * 256
+
+ def __getitem__(
+ self, ix: int
+ ) -> (
+ tuple[
+ tuple[int, int],
+ tuple[int, int, int, int],
+ tuple[int, int, int, int],
+ Image.Image,
+ ]
+ | None
+ ):
return self.glyph[ix]
- def compile(self):
+ def compile(self) -> None:
"""Create metrics and bitmap"""
if self.bitmap:
@@ -51,7 +72,7 @@ def compile(self):
# create bitmap large enough to hold all data
h = w = maxwidth = 0
lines = 1
- for glyph in self:
+ for glyph in self.glyph:
if glyph:
d, dst, src, im = glyph
h = max(h, src[3] - src[1])
@@ -65,13 +86,16 @@ def compile(self):
ysize = lines * h
if xsize == 0 and ysize == 0:
- return ""
+ return
self.ysize = h
# paste glyphs into bitmap
self.bitmap = Image.new("1", (xsize, ysize))
- self.metrics = [None] * 256
+ self.metrics: list[
+ tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]]
+ | None
+ ] = [None] * 256
x = y = 0
for i in range(256):
glyph = self[i]
@@ -88,12 +112,15 @@ def compile(self):
self.bitmap.paste(im.crop(src), s)
self.metrics[i] = d, dst, s
- def save(self, filename):
+ def save(self, filename: str) -> None:
"""Save font"""
self.compile()
# font data
+ if not self.bitmap:
+ msg = "No bitmap created"
+ raise ValueError(msg)
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
# font metrics
@@ -104,6 +131,6 @@ def save(self, filename):
for id in range(256):
m = self.metrics[id]
if not m:
- puti16(fp, [0] * 10)
+ puti16(fp, (0,) * 10)
else:
puti16(fp, m[0] + m[1] + m[2])
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index d84876eb6b6..7bb4736af13 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -27,6 +27,8 @@ class is not registered for use with :py:func:`PIL.Image.open()`. To open a
"""
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 @@ def _open(self):
]
-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 49e3cfe9b30..ca28cd3c774 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -92,7 +92,7 @@ class DecompressionBombError(Exception):
raise ImportError(msg)
except ImportError as v:
- core = DeferredError(ImportError("The _imaging C module is not installed."))
+ core = DeferredError.new(ImportError("The _imaging C module is not installed."))
# Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for
@@ -242,7 +242,7 @@ def _conv_type_shape(im):
_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.
@@ -530,15 +530,19 @@ def _new(self, im):
def __enter__(self):
return self
+ def _close_fp(self):
+ if getattr(self, "_fp", False):
+ if self._fp != self.fp:
+ self._fp.close()
+ self._fp = DeferredError(ValueError("Operation on closed image"))
+ if self.fp:
+ self.fp.close()
+
def __exit__(self, *args):
- if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
- if getattr(self, "_fp", False):
- if self._fp != self.fp:
- self._fp.close()
- self._fp = DeferredError(ValueError("Operation on closed image"))
- if self.fp:
- self.fp.close()
- self.fp = None
+ if hasattr(self, "fp"):
+ if getattr(self, "_exclusive_fp", False):
+ self._close_fp()
+ self.fp = None
def close(self):
"""
@@ -554,12 +558,7 @@ def close(self):
"""
if hasattr(self, "fp"):
try:
- if getattr(self, "_fp", False):
- if self._fp != self.fp:
- self._fp.close()
- self._fp = DeferredError(ValueError("Operation on closed image"))
- if self.fp:
- self.fp.close()
+ self._close_fp()
self.fp = None
except Exception as msg:
logger.debug("Error closing: %s", msg)
@@ -584,7 +583,9 @@ def _ensure_mutable(self):
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
@@ -709,7 +710,7 @@ def __setstate__(self, state):
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.
@@ -787,7 +788,7 @@ def tobitmap(self, name="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.
@@ -874,7 +875,7 @@ def verify(self):
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
@@ -1194,7 +1195,7 @@ def copy(self) -> Image:
__copy__ = copy
- def crop(self, box=None):
+ def crop(self, box=None) -> Image:
"""
Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel
@@ -1296,7 +1297,7 @@ def filter(self, filter):
]
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").
@@ -1306,7 +1307,7 @@ def getbands(self):
"""
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.
@@ -1659,7 +1660,7 @@ def entropy(self, mask=None, extrema=None):
return self.im.entropy(extrema)
return self.im.entropy()
- def paste(self, im, box=None, mask=None):
+ def paste(self, im, box=None, mask=None) -> None:
"""
Pastes another image into this image. The box argument is either
a 2-tuple giving the upper left corner, a 4-tuple defining the
@@ -2352,7 +2353,7 @@ def transform(x, y, matrix):
(w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor
)
- def save(self, fp, format=None, **params):
+ def save(self, fp, format=None, **params) -> None:
"""
Saves this image under the given filename. If no format is
specified, the format to use is determined from the filename
@@ -2511,7 +2512,7 @@ def show(self, title=None):
_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,
@@ -2661,7 +2662,7 @@ def transform(
resample=Resampling.NEAREST,
fill=1,
fillcolor=None,
- ):
+ ) -> Image:
"""
Transforms this image. This method creates a new image with the
given size, and the same mode as the original, and copies data
@@ -2684,6 +2685,10 @@ class Example(Image.ImageTransformHandler):
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::
@@ -2920,7 +2925,7 @@ def _check_size(size):
return True
-def new(mode, size, color=0):
+def new(mode, size, color=0) -> Image:
"""
Creates a new image with the given mode and size.
@@ -2959,7 +2964,7 @@ def new(mode, size, color=0):
return im._new(core.fill(mode, size, color))
-def frombytes(mode, size, data, decoder_name="raw", *args):
+def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
"""
Creates a copy of an image memory from pixel data in a buffer.
@@ -3208,7 +3213,7 @@ def _decompression_bomb_check(size):
)
-def open(fp, mode="r", formats=None):
+def open(fp, mode="r", formats=None) -> Image:
"""
Opens and identifies the given image file.
@@ -3433,7 +3438,7 @@ def merge(mode, bands):
# Plugin registry
-def register_open(id, factory, accept=None):
+def register_open(id, factory, accept=None) -> None:
"""
Register an image file plugin. This function should not be used
in application code.
@@ -3449,7 +3454,7 @@ def register_open(id, factory, accept=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.
@@ -3464,7 +3469,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.
@@ -3487,7 +3492,7 @@ def register_save_all(id, driver):
SAVE_ALL[id.upper()] = driver
-def register_extension(id, extension):
+def register_extension(id, extension) -> None:
"""
Registers an image extension. This function should not be
used in application code.
@@ -3498,7 +3503,7 @@ def register_extension(id, extension):
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.
@@ -3519,7 +3524,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 9d27f2513a7..3e40105e46a 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
@@ -28,9 +35,9 @@
# anything in core.
from ._util import DeferredError
- _imagingcms = DeferredError(ex)
+ _imagingcms = DeferredError.new(ex)
-DESCRIPTION = """
+_DESCRIPTION = """
pyCMS
a Python / PIL interface to the littleCMS ICC Color Management System
@@ -93,7 +100,22 @@
"""
-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 @@ class Direction(IntEnum):
"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 @@ def __init__(
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/ImageColor.py b/src/PIL/ImageColor.py
index bfad27c82d6..ad59b066779 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -19,10 +19,12 @@
from __future__ import annotations
import re
+from functools import lru_cache
from . import Image
+@lru_cache
def getrgb(color):
"""
Convert a color string to an RGB or RGBA tuple. If the string cannot be
@@ -121,6 +123,7 @@ def getrgb(color):
raise ValueError(msg)
+@lru_cache
def getcolor(color, mode):
"""
Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index ae4e23db17b..5ba5a6f8277 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -32,7 +32,7 @@
import itertools
import struct
import sys
-from typing import NamedTuple
+from typing import Any, NamedTuple
from . import Image
from ._deprecate import deprecate
@@ -94,7 +94,7 @@ class _Tile(NamedTuple):
encoder_name: str
extents: tuple[int, int, int, int]
offset: int
- args: tuple | str | None
+ args: tuple[Any, ...] | str | None
#
@@ -514,7 +514,7 @@ def close(self):
# --------------------------------------------------------------------
-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 @@ def extents(self):
class PyCodec:
+ fd: io.BytesIO | None
+
def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
@@ -713,7 +715,7 @@ def decode(self, buffer):
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/ImageFilter.py b/src/PIL/ImageFilter.py
index 021b40c0ef4..035b83c4d77 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -396,7 +396,7 @@ def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
if hasattr(table, "shape"):
try:
import numpy
- except ImportError: # pragma: no cover
+ except ImportError:
pass
if numpy and isinstance(table, numpy.ndarray):
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 6db7cc4eccb..a63b73b33f5 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -34,7 +34,7 @@
from enum import IntEnum
from io import BytesIO
from pathlib import Path
-from typing import IO
+from typing import BinaryIO
from . import Image
from ._util import is_directory, is_path
@@ -53,7 +53,7 @@ class Layout(IntEnum):
except ImportError as ex:
from ._util import DeferredError
- core = DeferredError(ex)
+ core = DeferredError.new(ex)
def _string_length_check(text):
@@ -149,6 +149,8 @@ def getmask(self, text, mode="", *args, **kwargs):
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.
"""
+ _string_length_check(text)
+ Image._decompression_bomb_check(self.font.getsize(text))
return self.font.getmask(text, mode)
def getbbox(self, text, *args, **kwargs):
@@ -191,7 +193,7 @@ class FreeTypeFont:
def __init__(
self,
- font: bytes | str | Path | IO | None = None,
+ font: bytes | str | Path | BinaryIO | None = None,
size: float = 10,
index: int = 0,
encoding: str = "",
@@ -582,22 +584,13 @@ def getmask2(
_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
-
- im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
- return im
+ Image._decompression_bomb_check(size)
+ return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
- offset = self.font.render(
+ return self.font.render(
text,
fill,
mode,
@@ -610,8 +603,6 @@ def fill(width, height):
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 7ca512e7568..a7652f237ed 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 @@
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 @@ def __fixup(self, im1):
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 @@ def __eq__(self, other):
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,12 @@ 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"
+ raise ValueError(msg)
+
args.update(_dict)
args.update(kw)
for k, v in args.items():
@@ -242,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 282e7d2a54e..534c6291a02 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 @@ def __init__(self, patterns=None, op_name=None):
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 @@ def _pattern_permute(self, basic_pattern, options, basic_result):
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 @@ def build_lut(self):
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 @@ def build_lut(self):
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 @@ def build_lut(self):
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 @@ def __init__(self, lut=None, op_name=None, patterns=None):
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 @@ def apply(self, image):
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 @@ def match(self, image):
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 @@ def get_on_pixels(self, image):
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 @@ def load_lut(self, filename):
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 @@ def save_lut(self, filename):
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 fbcfa309d29..2b6cecc6105 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -192,7 +192,7 @@ def save(self, fp):
# 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 fad3e098003..c03122c11aa 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 @@ def show(self, image, **options):
# 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 @@ def get_command(self, file, **options):
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 @@ def show_file(self, path, **options):
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 @@ def show_file(self, path, **options):
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 @@ def get_command_ex(self, file, title=None, **options):
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 @@ def show_file(self, path, **options):
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 1fdaa9140ff..6aa82dadd9c 100644
--- a/src/PIL/ImageTransform.py
+++ b/src/PIL/ImageTransform.py
@@ -14,17 +14,29 @@
#
from __future__ import annotations
+from typing import Sequence
+
from . import Image
class Transform(Image.ImageTransformHandler):
- def __init__(self, data):
+ """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):
+ def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
return self.method, self.data
- def transform(self, size, image, **options):
+ def transform(
+ self,
+ size: tuple[int, int],
+ 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)
@@ -42,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.
@@ -51,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.
@@ -64,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`.
@@ -80,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
@@ -95,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 7469c592dd2..abb3fb762e7 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/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index e7dc3e4e4d2..4096094348a 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -16,30 +16,48 @@
#
from __future__ import annotations
-import os
-import tempfile
+from io import BytesIO
+from typing import Sequence
from . import Image, ImageFile
-from ._binary import i8, o8
from ._binary import i16be as i16
from ._binary import i32be as i32
+from ._deprecate import deprecate
COMPRESSION = {1: "raw", 5: "jpeg"}
-PAD = o8(0) * 4
+
+def __getattr__(name: str) -> bytes:
+ if name == "PAD":
+ deprecate("IptcImagePlugin.PAD", 12)
+ return b"\0\0\0\0"
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
#
# Helpers
-def i(c):
- return i32((PAD + c)[-4:])
+def _i(c: bytes) -> int:
+ return i32((b"\0\0\0\0" + c)[-4:])
+
+
+def _i8(c: int | bytes) -> int:
+ return c if isinstance(c, int) else c[0]
+
+def i(c: bytes) -> int:
+ """.. deprecated:: 10.2.0"""
+ deprecate("IptcImagePlugin.i", 12)
+ return _i(c)
-def dump(c):
+
+def dump(c: Sequence[int | bytes]) -> None:
+ """.. deprecated:: 10.2.0"""
+ deprecate("IptcImagePlugin.dump", 12)
for i in c:
- print("%02x" % i8(i), end=" ")
+ print("%02x" % _i8(i), end=" ")
print()
@@ -52,10 +70,10 @@ class IptcImageFile(ImageFile.ImageFile):
format = "IPTC"
format_description = "IPTC/NAA"
- def getint(self, key):
- return i(self.info[key])
+ def getint(self, key: tuple[int, int]) -> int:
+ return _i(self.info[key])
- def field(self):
+ def field(self) -> tuple[tuple[int, int] | None, int]:
#
# get a IPTC field header
s = self.fp.read(5)
@@ -77,13 +95,13 @@ def field(self):
elif size == 128:
size = 0
elif size > 128:
- size = i(self.fp.read(size - 128))
+ size = _i(self.fp.read(size - 128))
else:
size = i16(s, 3)
return tag, size
- def _open(self):
+ def _open(self) -> None:
# load descriptive fields
while True:
offset = self.fp.tell()
@@ -103,10 +121,10 @@ def _open(self):
self.info[tag] = tagdata
# mode
- layers = i8(self.info[(3, 60)][0])
- component = i8(self.info[(3, 60)][1])
+ layers = self.info[(3, 60)][0]
+ component = self.info[(3, 60)][1]
if (3, 65) in self.info:
- id = i8(self.info[(3, 65)][0]) - 1
+ id = self.info[(3, 65)][0] - 1
else:
id = 0
if layers == 1 and not component:
@@ -128,27 +146,22 @@ def _open(self):
# tile
if tag == (8, 10):
- self.tile = [
- ("iptc", (compression, offset), (0, 0, self.size[0], self.size[1]))
- ]
+ self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
def load(self):
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
- type, tile, box = self.tile[0]
-
- encoding, offset = tile
+ offset, compression = self.tile[0][2:]
self.fp.seek(offset)
# Copy image data to temporary file
- o_fd, outfile = tempfile.mkstemp(text=False)
- o = os.fdopen(o_fd)
- if encoding == "raw":
+ o = BytesIO()
+ if compression == "raw":
# To simplify access to the extracted file,
# prepend a PPM header
- o.write("P5\n%d %d\n255\n" % self.size)
+ o.write(b"P5\n%d %d\n255\n" % self.size)
while True:
type, size = self.field()
if type != (8, 10):
@@ -159,17 +172,10 @@ def load(self):
break
o.write(s)
size -= len(s)
- o.close()
- try:
- with Image.open(outfile) as _im:
- _im.load()
- self.im = _im.im
- finally:
- try:
- os.unlink(outfile)
- except OSError:
- pass
+ with Image.open(o) as _im:
+ _im.load()
+ self.im = _im.im
Image.register_open(IptcImageFile.format, IptcImageFile)
@@ -185,8 +191,6 @@ def getiptcinfo(im):
:returns: A dictionary containing IPTC information, or None if
no IPTC information block was found.
"""
- import io
-
from . import JpegImagePlugin, TiffImagePlugin
data = None
@@ -221,7 +225,7 @@ class FakeImage:
# parse the IPTC information chunk
im.info = {}
- im.fp = io.BytesIO(data)
+ im.fp = BytesIO(data)
try:
im._open()
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 59bade303f8..81b8749a332 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -87,10 +87,12 @@ def APP(self, marker):
self.info["dpi"] = jfif_density
self.info["jfif_unit"] = jfif_unit
self.info["jfif_density"] = jfif_density
- elif marker == 0xFFE1 and s[:5] == b"Exif\0":
- if "exif" not in self.info:
- # extract EXIF information (incomplete)
- self.info["exif"] = s # FIXME: value will change
+ elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
+ # extract EXIF information
+ if "exif" in self.info:
+ self.info["exif"] += s[6:]
+ else:
+ self.info["exif"] = s
self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
@@ -783,6 +785,7 @@ def validate_qtables(qtables):
progressive,
info.get("smooth", 0),
optimize,
+ info.get("keep_rgb", False),
info.get("streamtype", 0),
dpi[0],
dpi[1],
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index 9a85c0d15b0..27972236c0a 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -22,8 +22,8 @@
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 f4e598ca3a0..b9e9243e59f 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 @@
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 @@ def peek(self, bits):
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 77dac65b6b3..bb7e466a790 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -35,7 +35,7 @@
# 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 @@ def decode(self, buffer):
# 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 a0515b302eb..1cd5c4a9dbe 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 @@ def _open(self):
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/PcfFontFile.py b/src/PIL/PcfFontFile.py
index d602a163349..0d1968b140a 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import io
+from typing import BinaryIO, Callable
from . import FontFile, Image
from ._binary import i8
@@ -41,7 +42,7 @@
PCF_GLYPH_NAMES = 1 << 7
PCF_BDF_ACCELERATORS = 1 << 8
-BYTES_PER_ROW = [
+BYTES_PER_ROW: list[Callable[[int], int]] = [
lambda bits: ((bits + 7) >> 3),
lambda bits: ((bits + 15) >> 3) & ~1,
lambda bits: ((bits + 31) >> 3) & ~3,
@@ -49,7 +50,7 @@
]
-def sz(s, o):
+def sz(s: bytes, o: int) -> bytes:
return s[o : s.index(b"\0", o)]
@@ -58,7 +59,7 @@ class PcfFontFile(FontFile.FontFile):
name = "name"
- def __init__(self, fp, charset_encoding="iso8859-1"):
+ def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"):
self.charset_encoding = charset_encoding
magic = l32(fp.read(4))
@@ -104,7 +105,9 @@ def __init__(self, fp, charset_encoding="iso8859-1"):
bitmaps[ix],
)
- def _getformat(self, tag):
+ def _getformat(
+ self, tag: int
+ ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]:
format, size, offset = self.toc[tag]
fp = self.fp
@@ -119,7 +122,7 @@ def _getformat(self, tag):
return fp, format, i16, i32
- def _load_properties(self):
+ def _load_properties(self) -> dict[bytes, bytes | int]:
#
# font properties
@@ -138,18 +141,16 @@ def _load_properties(self):
data = fp.read(i32(fp.read(4)))
for k, s, v in p:
- k = sz(data, k)
- if s:
- v = sz(data, v)
- properties[k] = v
+ property_value: bytes | int = sz(data, v) if s else v
+ properties[sz(data, k)] = property_value
return properties
- def _load_metrics(self):
+ def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]:
#
# font metrics
- metrics = []
+ metrics: list[tuple[int, int, int, int, int, int, int, int]] = []
fp, format, i16, i32 = self._getformat(PCF_METRICS)
@@ -182,7 +183,9 @@ def _load_metrics(self):
return metrics
- def _load_bitmaps(self, metrics):
+ def _load_bitmaps(
+ self, metrics: list[tuple[int, int, int, int, int, int, int, int]]
+ ) -> list[Image.Image]:
#
# bitmap data
@@ -222,7 +225,7 @@ def _load_bitmaps(self, metrics):
return bitmaps
- def _load_encoding(self):
+ def _load_encoding(self) -> list[int | None]:
fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS)
first_col, last_col = i16(fp.read(2)), i16(fp.read(2))
@@ -233,7 +236,7 @@ def _load_encoding(self):
nencoding = (last_col - first_col + 1) * (last_row - first_row + 1)
# map character code to bitmap index
- encoding = [None] * min(256, nencoding)
+ encoding: list[int | None] = [None] * min(256, nencoding)
encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)]
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 98ecefd0514..3e0968a8386 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -37,7 +37,7 @@
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 @@ def _open(self):
}
-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 af866feb362..887b6568bf7 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -27,7 +27,7 @@
# 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 3c37e448bdc..8108f97bdc9 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -378,7 +378,7 @@ def save_rewind(self):
}
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 25dbfa5b0bc..3e45ba95c84 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 @@
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 @@
}
-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 @@ def _read_magic(self):
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 @@ def _read_token(self):
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 @@ def _open(self):
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)
+
+ 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, 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)]
+ 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 @@ def _open(self):
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 @@ def _ignore_comments(self, block):
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 @@ def _decode_bitonal(self):
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 @@ def _decode_blocks(self, maxval):
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 @@ def _decode_blocks(self, maxval):
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 @@ def _decode_blocks(self, maxval):
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 @@ def decode(self, buffer):
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 @@ def decode(self, buffer):
# --------------------------------------------------------------------
-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 @@ def _save(im, fp, filename):
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/PyAccess.py b/src/PIL/PyAccess.py
index 23ff154f6cc..07bb712d83e 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -43,7 +43,7 @@
# anything in core.
from ._util import DeferredError
- FFI = ffi = DeferredError(ex)
+ FFI = ffi = DeferredError.new(ex)
logger = logging.getLogger(__name__)
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index f9a10f6109c..ccf661ff1f3 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -24,13 +24,14 @@
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 @@ def _open(self):
]
-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 11ce3dfefd0..4e098474ab9 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -21,7 +21,7 @@
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 @@ def _open(self):
# 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/TarIO.py b/src/PIL/TarIO.py
index c9923487d5e..7470663b4a1 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -21,7 +21,7 @@
from . import ContainerIO
-class TarIO(ContainerIO.ContainerIO):
+class TarIO(ContainerIO.ContainerIO[bytes]):
"""A file object that provides read access to a given member of a TAR file."""
def __init__(self, tarfile: str, file: str) -> None:
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 65c7484f756..584932d2c7d 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 @@ def _open(self):
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 @@ def load_end(self):
}
-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/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 254ce89aec0..f92fc0e0b84 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1704,25 +1704,27 @@ def _save(im, fp, filename):
colormap += [0] * (256 - colors)
ifd[COLORMAP] = colormap
# data orientation
- stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
- # aim for given strip size (64 KB by default) when using libtiff writer
- if libtiff:
- im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
- rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
- # JPEG encoder expects multiple of 8 rows
- if compression == "jpeg":
- rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
- else:
- rows_per_strip = im.size[1]
- if rows_per_strip == 0:
- rows_per_strip = 1
- strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
- strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
- ifd[ROWSPERSTRIP] = rows_per_strip
+ w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
+ stride = len(bits) * ((w * bits[0] + 7) // 8)
+ if ROWSPERSTRIP not in ifd:
+ # aim for given strip size (64 KB by default) when using libtiff writer
+ if libtiff:
+ im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
+ rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
+ # JPEG encoder expects multiple of 8 rows
+ if compression == "jpeg":
+ rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
+ else:
+ rows_per_strip = h
+ if rows_per_strip == 0:
+ rows_per_strip = 1
+ ifd[ROWSPERSTRIP] = rows_per_strip
+ strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
+ strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
if strip_byte_counts >= 2**16:
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
- stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
+ stride * h - strip_byte_counts * (strips_per_image - 1),
)
ifd[STRIPOFFSETS] = tuple(
range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index 47ba1c54803..c84adaca215 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -33,7 +33,7 @@
)
-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 566acbfe5af..0291e2858ac 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 @@
)
-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 @@ def _open(self):
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/_binary.py b/src/PIL/_binary.py
index c60c9cec1b7..0a07e8d0e12 100644
--- a/src/PIL/_binary.py
+++ b/src/PIL/_binary.py
@@ -18,16 +18,16 @@
from struct import pack, unpack_from
-def i8(c):
- return c if c.__class__ is int else c[0]
+def i8(c: bytes) -> int:
+ return c[0]
-def o8(i):
+def o8(i: int) -> bytes:
return bytes((i & 255,))
# Input, le = little endian, be = big endian
-def i16le(c, o=0):
+def i16le(c: bytes, o: int = 0) -> int:
"""
Converts a 2-bytes (16 bits) string to an unsigned integer.
@@ -37,7 +37,7 @@ def i16le(c, o=0):
return unpack_from(" int:
"""
Converts a 2-bytes (16 bits) string to a signed integer.
@@ -47,7 +47,7 @@ def si16le(c, o=0):
return unpack_from(" int:
"""
Converts a 2-bytes (16 bits) string to a signed integer, big endian.
@@ -57,7 +57,7 @@ def si16be(c, o=0):
return unpack_from(">h", c, o)[0]
-def i32le(c, o=0):
+def i32le(c: bytes, o: int = 0) -> int:
"""
Converts a 4-bytes (32 bits) string to an unsigned integer.
@@ -67,7 +67,7 @@ def i32le(c, o=0):
return unpack_from(" int:
"""
Converts a 4-bytes (32 bits) string to a signed integer.
@@ -77,26 +77,26 @@ def si32le(c, o=0):
return unpack_from(" int:
return unpack_from(">H", c, o)[0]
-def i32be(c, o=0):
+def i32be(c: bytes, o: int = 0) -> int:
return unpack_from(">I", c, o)[0]
# Output, le = little endian, be = big endian
-def o16le(i):
+def o16le(i: int) -> bytes:
return pack(" bytes:
return pack(" bytes:
return pack(">H", i)
-def o32be(i):
+def o32be(i: int) -> bytes:
return pack(">I", i)
diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi
new file mode 100644
index 00000000000..b0235555dc5
--- /dev/null
+++ b/src/PIL/_imagingcms.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi
new file mode 100644
index 00000000000..b0235555dc5
--- /dev/null
+++ b/src/PIL/_imagingft.pyi
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi
new file mode 100644
index 00000000000..b0235555dc5
--- /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 00000000000..b0235555dc5
--- /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/PIL/_typing.py b/src/PIL/_typing.py
new file mode 100644
index 00000000000..608b2b41fa8
--- /dev/null
+++ b/src/PIL/_typing.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import sys
+
+if sys.version_info >= (3, 10):
+ from typing import TypeGuard
+else:
+ try:
+ from typing_extensions import TypeGuard
+ except ImportError:
+ from typing import Any
+
+ class TypeGuard: # type: ignore[no-redef]
+ def __class_getitem__(cls, item: Any) -> type[bool]:
+ return bool
+
+
+__all__ = ["TypeGuard"]
diff --git a/src/PIL/_util.py b/src/PIL/_util.py
index 4634d335bba..13f369cca1d 100644
--- a/src/PIL/_util.py
+++ b/src/PIL/_util.py
@@ -2,20 +2,31 @@
import os
from pathlib import Path
+from typing import Any, NoReturn
+from ._typing import TypeGuard
-def is_path(f):
+
+def is_path(f: Any) -> TypeGuard[bytes | str | Path]:
return isinstance(f, (bytes, str, Path))
-def is_directory(f):
+def is_directory(f: Any) -> TypeGuard[bytes | str | Path]:
"""Checks if an object is a string, and that it points to a directory."""
return is_path(f) and os.path.isdir(f)
class DeferredError:
- def __init__(self, ex):
+ def __init__(self, ex: BaseException):
self.ex = ex
- def __getattr__(self, elt):
+ def __getattr__(self, elt: str) -> NoReturn:
raise self.ex
+
+ @staticmethod
+ def new(ex: BaseException) -> Any:
+ """
+ Creates an object that raises the wrapped exception ``ex`` when used,
+ and casts it to :py:obj:`~typing.Any` type.
+ """
+ return DeferredError(ex)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 7d994caf4d7..0568943b52b 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
-__version__ = "10.2.0.dev0"
+__version__ = "10.3.0.dev0"
diff --git a/src/_imaging.c b/src/_imaging.c
index 2270c77fe7e..59f80a35415 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -2649,6 +2649,26 @@ _font_new(PyObject *self_, PyObject *args) {
self->glyphs[i].sy0 = S16(B16(glyphdata, 14));
self->glyphs[i].sx1 = S16(B16(glyphdata, 16));
self->glyphs[i].sy1 = S16(B16(glyphdata, 18));
+
+ // Do not allow glyphs to extend beyond bitmap image
+ // Helps prevent DOS by stopping cropped images being larger than the original
+ if (self->glyphs[i].sx0 < 0) {
+ self->glyphs[i].dx0 -= self->glyphs[i].sx0;
+ self->glyphs[i].sx0 = 0;
+ }
+ if (self->glyphs[i].sy0 < 0) {
+ self->glyphs[i].dy0 -= self->glyphs[i].sy0;
+ self->glyphs[i].sy0 = 0;
+ }
+ if (self->glyphs[i].sx1 > self->bitmap->xsize) {
+ self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize;
+ self->glyphs[i].sx1 = self->bitmap->xsize;
+ }
+ if (self->glyphs[i].sy1 > self->bitmap->ysize) {
+ self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize;
+ self->glyphs[i].sy1 = self->bitmap->ysize;
+ }
+
if (self->glyphs[i].dy0 < y0) {
y0 = self->glyphs[i].dy0;
}
@@ -2721,7 +2741,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) {
static PyObject *
_font_getmask(ImagingFontObject *self, PyObject *args) {
Imaging im;
- Imaging bitmap;
+ Imaging bitmap = NULL;
int x, b;
int i = 0;
int status;
@@ -2730,7 +2750,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
PyObject *encoded_string;
unsigned char *text;
- char *mode = "";
+ char *mode;
if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) {
return NULL;
@@ -2753,10 +2773,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
b = self->baseline;
for (x = 0; text[i]; i++) {
glyph = &self->glyphs[text[i]];
- bitmap =
- ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
- if (!bitmap) {
- goto failed;
+ if (i == 0 || text[i] != text[i - 1]) {
+ ImagingDelete(bitmap);
+ bitmap =
+ ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1);
+ if (!bitmap) {
+ goto failed;
+ }
}
status = ImagingPaste(
im,
@@ -2766,17 +2789,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) {
glyph->dy0 + b,
glyph->dx1 + x,
glyph->dy1 + b);
- ImagingDelete(bitmap);
if (status < 0) {
goto failed;
}
x = x + glyph->dx;
b = b + glyph->dy;
}
+ ImagingDelete(bitmap);
free(text);
return PyImagingNew(im);
failed:
+ ImagingDelete(bitmap);
free(text);
ImagingDelete(im);
Py_RETURN_NONE;
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 68c66ac2c60..6e24fcf95ed 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/encode.c b/src/encode.c
index 4664ad0f32a..c7dd510150e 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
Py_ssize_t progressive = 0;
Py_ssize_t smooth = 0;
Py_ssize_t optimize = 0;
+ int keep_rgb = 0;
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
Py_ssize_t xdpi = 0, ydpi = 0;
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
@@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "ss|nnnnnnnnnnOz#y#y#",
+ "ss|nnnnpnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
&progressive,
&smooth,
&optimize,
+ &keep_rgb,
&streamtype,
&xdpi,
&ydpi,
@@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
+ ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb;
((JPEGENCODERSTATE *)encoder->state.context)->quality = quality;
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;
diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c
index f232454052a..e37301df765 100644
--- a/src/libImaging/GifEncode.c
+++ b/src/libImaging/GifEncode.c
@@ -105,7 +105,7 @@ static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr,
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 5cc74e69bf5..7cdba902281 100644
--- a/src/libImaging/Jpeg.h
+++ b/src/libImaging/Jpeg.h
@@ -74,6 +74,9 @@ typedef struct {
/* Optimize Huffman tables (slow) */
int optimize;
+ /* Disable automatic conversion of RGB images to YCbCr if non-zero */
+ int keep_rgb;
+
/* Stream type (0=full, 1=tables only, 2=image only) */
int streamtype;
diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c
index 9da830b186f..00f3d5f74db 100644
--- a/src/libImaging/JpegEncode.c
+++ b/src/libImaging/JpegEncode.c
@@ -137,6 +137,30 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
/* Compressor configuration */
jpeg_set_defaults(&context->cinfo);
+ /* Prevent RGB -> YCbCr conversion */
+ if (context->keep_rgb) {
+ switch (context->cinfo.in_color_space) {
+ case JCS_RGB:
+#ifdef JCS_EXTENSIONS
+ case JCS_EXT_RGBX:
+#endif
+ switch (context->subsampling) {
+ case -1: /* Default */
+ case 0: /* No subsampling */
+ break;
+ default:
+ /* Would subsample the green and blue
+ channels, which doesn't make sense */
+ state->errcode = IMAGING_CODEC_CONFIG;
+ return -1;
+ }
+ jpeg_set_colorspace(&context->cinfo, JCS_RGB);
+ break;
+ default:
+ break;
+ }
+ }
+
/* Use custom quantization tables */
if (context->qtables) {
int i;
diff --git a/tox.ini b/tox.ini
index 034d893721b..fb6746ce7bf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -33,7 +33,10 @@ commands =
[testenv:mypy]
skip_install = true
deps =
+ ipython
mypy==1.7.1
numpy
+extras =
+ typing
commands =
mypy src {posargs}
diff --git a/winbuild/build.rst b/winbuild/build.rst
index a8e4ebaa6cc..cd3b559e7f0 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 8e3757ca89e..df33ea493e6 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -105,7 +105,7 @@ def cmd_msbuild(
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 @@ def cmd_msbuild(
"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 @@ def cmd_msbuild(
"#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 @@ def cmd_msbuild(
*cmds_cmake(
"tiff",
"-DBUILD_SHARED_LIBS:BOOL=OFF",
+ "-DWebP_LIBRARY=libwebp",
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
)
],
@@ -651,7 +651,7 @@ def build_dep_all() -> None:
(
"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)",