Merge branch 'python-pillow:main' into main

This commit is contained in:
Sascha Ronnie Daoudia 2024-01-18 22:45:38 +01:00 committed by GitHub
commit d2e54e9601
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 929 additions and 434 deletions

View File

@ -14,7 +14,7 @@ environment:
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64 - PYTHON: C:/Python38-x64
ARCHITECTURE: x64 ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:

View File

@ -2,11 +2,12 @@ name: Test Windows
on: on:
push: push:
branches:
- "**"
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -14,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:

View File

@ -30,7 +30,64 @@ env:
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: 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 }} name: ${{ matrix.name }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
@ -62,9 +119,12 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
- name: Build wheels - name: Install cibuildwheel
run: | run: |
python3 -m pip install -r .ci/requirements-cibw.txt python3 -m pip install -r .ci/requirements-cibw.txt
- name: Build wheels
run: |
python3 -m cibuildwheel --output-dir wheelhouse python3 -m cibuildwheel --output-dir wheelhouse
env: env:
CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_ARCHS: ${{ matrix.cibw_arch }}
@ -81,18 +141,15 @@ jobs:
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
windows: windows:
name: Windows ${{ matrix.arch }} name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- arch: x86 - cibw_arch: x86
cibw_arch: x86 - cibw_arch: AMD64
- arch: x64 - cibw_arch: ARM64
cibw_arch: AMD64
- arch: ARM64
cibw_arch: ARM64
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -106,6 +163,10 @@ jobs:
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install cibuildwheel
run: |
python.exe -m pip install -r .ci/requirements-cibw.txt
- name: Prepare for build - name: Prepare for build
run: | run: |
choco install nasm --no-progress choco install nasm --no-progress
@ -114,9 +175,7 @@ jobs:
# Install extra test images # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\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.cibw_arch }}
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }}
shell: pwsh shell: pwsh
- name: Build wheels - name: Build wheels
@ -157,13 +216,13 @@ jobs:
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: dist-windows-${{ matrix.arch }} name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
- name: Upload fribidi.dll - name: Upload fribidi.dll
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: fribidi-windows-${{ matrix.arch }} name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi* path: winbuild\build\bin\fribidi*
sdist: sdist:
@ -187,7 +246,7 @@ jobs:
pypi-publish: pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 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 runs-on: ubuntu-latest
name: Upload release to PyPI name: Upload release to PyPI
environment: environment:

View File

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

View File

@ -2,6 +2,27 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
10.3.0 (unreleased)
-------------------
- Fix APNG info after seeking backwards more than twice #7701
[esoma, radarhere]
- Deprecate ImageCms constants and versions() function #7702
[nulano, radarhere]
- Added PerspectiveTransform #7699
[radarhere]
- Add support for reading and writing grayscale PFM images #7696
[nulano, hugovk]
- Add LCMS2 flags to ImageCms #7676
[nulano, radarhere, hugovk]
- Rename x64 to AMD64 in winbuild #7693
[nulano]
10.2.0 (2024-01-02) 10.2.0 (2024-01-02)
------------------- -------------------

View File

@ -48,9 +48,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions build status (Wheels)" alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
<a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
alt="Travis CI wheels build status (aarch64)"
src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a> src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
@ -68,10 +65,10 @@ As of 2019, Pillow development is
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img <a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
alt="Tidelift" alt="Tidelift"
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a> src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/pillow/"><img
alt="Newest PyPI version" alt="Newest PyPI version"
src="https://img.shields.io/pypi/v/pillow.svg"></a> src="https://img.shields.io/pypi/v/pillow.svg"></a>
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/pillow/"><img
alt="Number of PyPI downloads" alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a> src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://www.bestpractices.dev/projects/6331"><img <a href="https://www.bestpractices.dev/projects/6331"><img

View File

@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch. * [ ] Develop and prepare release in `main` branch.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them. * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`. * [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@ -83,12 +83,6 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
has passed, including the "Upload release to PyPI" job. This will have been triggered has passed, including the "Upload release to PyPI" job. This will have been triggered
by the new tag. by the new tag.
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`. Check and upload them e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/pillow-5.2.0*
```
## Publicize Release ## Publicize Release

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

BIN
Tests/images/hopper.pfm Normal file

Binary file not shown.

BIN
Tests/images/hopper_be.pfm Normal file

Binary file not shown.

View File

@ -689,3 +689,12 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat
) )
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
assert reloaded.mode == mode 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

View File

@ -270,7 +270,7 @@ def test_render_scale1():
image1_scale1_compare.load() image1_scale1_compare.load()
assert_image_similar(image1_scale1, image1_scale1_compare, 5) assert_image_similar(image1_scale1, image1_scale1_compare, 5)
# Non-Zero bounding box # Non-zero bounding box
with Image.open(FILE2) as image2_scale1: with Image.open(FILE2) as image2_scale1:
image2_scale1.load() image2_scale1.load()
with Image.open(FILE2_COMPARE) as image2_scale1_compare: with Image.open(FILE2_COMPARE) as image2_scale1_compare:
@ -292,7 +292,7 @@ def test_render_scale2():
image1_scale2_compare.load() image1_scale2_compare.load()
assert_image_similar(image1_scale2, image1_scale2_compare, 5) assert_image_similar(image1_scale2, image1_scale2_compare, 5)
# Non-Zero bounding box # Non-zero bounding box
with Image.open(FILE2) as image2_scale2: with Image.open(FILE2) as image2_scale2:
image2_scale2.load(scale=2) image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:

View File

@ -6,7 +6,12 @@ import pytest
from PIL import Image, PpmImagePlugin 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 # sample ppm stream
TEST_FILE = "Tests/images/hopper.ppm" TEST_FILE = "Tests/images/hopper.ppm"
@ -84,20 +89,58 @@ def test_16bit_pgm():
def test_16bit_pgm_write(tmp_path): def test_16bit_pgm_write(tmp_path):
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
f = str(tmp_path / "temp.pgm") filename = str(tmp_path / "temp.pgm")
im.save(f, "PPM") im.save(filename, "PPM")
assert_image_equal_tofile(im, f) assert_image_equal_tofile(im, filename)
def test_pnm(tmp_path): def test_pnm(tmp_path):
with Image.open("Tests/images/hopper.pnm") as im: with Image.open("Tests/images/hopper.pnm") as im:
assert_image_similar(im, hopper(), 0.0001) assert_image_similar(im, hopper(), 0.0001)
f = str(tmp_path / "temp.pnm") filename = str(tmp_path / "temp.pnm")
im.save(f) im.save(filename)
assert_image_equal_tofile(im, f) assert_image_equal_tofile(im, filename)
def test_pfm(tmp_path):
with Image.open("Tests/images/hopper.pfm") as im:
assert im.info["scale"] == 1.0
assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm")
im.save(filename)
assert_image_equal_tofile(im, filename)
def test_pfm_big_endian(tmp_path):
with Image.open("Tests/images/hopper_be.pfm") as im:
assert im.info["scale"] == 2.5
assert_image_equal(im, hopper("F"))
filename = str(tmp_path / "tmp.pfm")
im.save(filename)
assert_image_equal_tofile(im, filename)
@pytest.mark.parametrize(
"data",
[
b"Pf 1 1 NaN \0\0\0\0",
b"Pf 1 1 inf \0\0\0\0",
b"Pf 1 1 -inf \0\0\0\0",
b"Pf 1 1 0.0 \0\0\0\0",
b"Pf 1 1 -0.0 \0\0\0\0",
],
)
def test_pfm_invalid(data):
with pytest.raises(ValueError):
with Image.open(BytesIO(data)):
pass
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -403,7 +403,7 @@ class TestCoreResampleCoefficients:
if px[2, 0] != test_color // 2: if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0] 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 # regression test for the wrong coefficients calculation
# due to bug https://github.com/python-pillow/Pillow/issues/2161 # due to bug https://github.com/python-pillow/Pillow/issues/2161
im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF))

View File

@ -10,18 +10,25 @@ from .helper import assert_image_equal, assert_image_similar, hopper
class TestImageTransform: class TestImageTransform:
def test_sanity(self): def test_sanity(self):
im = Image.new("L", (100, 100)) im = hopper()
seq = tuple(range(10)) for transform in (
ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)),
transform = ImageTransform.AffineTransform(seq[:6]) ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)),
im.transform((100, 100), transform) ImageTransform.ExtentTransform((0, 0) + im.size),
transform = ImageTransform.ExtentTransform(seq[:4]) ImageTransform.QuadTransform(
im.transform((100, 100), transform) (0, 0, 0, im.height, im.width, im.height, im.width, 0)
transform = ImageTransform.QuadTransform(seq[:8]) ),
im.transform((100, 100), transform) ImageTransform.MeshTransform(
transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) [
im.transform((100, 100), transform) (
(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): def test_info(self):
comment = b"File written by Adobe Photoshop\xa8 4.0" comment = b"File written by Adobe Photoshop\xa8 4.0"

View File

@ -49,8 +49,8 @@ def skip_missing():
def test_sanity(): def test_sanity():
# basic smoke test. # basic smoke test.
# this mostly follows the cms_test outline. # this mostly follows the cms_test outline.
with pytest.warns(DeprecationWarning):
v = ImageCms.versions() # should return four strings v = ImageCms.versions() # should return four strings
assert v[0] == "1.0.0 pil" assert v[0] == "1.0.0 pil"
assert list(map(type, v)) == [str, str, str, str] assert list(map(type, v)) == [str, str, str, str]
@ -90,6 +90,16 @@ def test_sanity():
hopper().point(t) 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(): def test_name():
skip_missing() skip_missing()
# get profile information for file # get profile information for file
@ -627,3 +637,12 @@ def test_rgb_lab(mode):
im = Image.new("LAB", (1, 1), (255, 0, 0)) im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode) converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) 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)

View File

@ -77,14 +77,6 @@ can be found here.
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL.ImageTransform` Module
---------------------------------
.. automodule:: PIL.ImageTransform
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.PaletteFile` Module :mod:`~PIL.PaletteFile` Module
------------------------------ ------------------------------

View File

@ -6,15 +6,14 @@ Goals
The fork author's goal is to foster and support active development of PIL through: 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`_ - Publicized development activity on `GitHub`_
- Regular releases to the `Python Package Index`_ - Regular releases to the `Python Package Index`_
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _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 .. _GitHub: https://github.com/python-pillow/Pillow
.. _Python Package Index: https://pypi.org/project/Pillow/ .. _Python Package Index: https://pypi.org/project/pillow/
License License
------- -------

View File

@ -55,6 +55,43 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
for internal use, so there is no replacement. They can each be replaced for internal use, so there is no replacement. They can each be replaced
by a single line of code using builtin functions in Python. 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 Removed features
---------------- ----------------
@ -118,7 +155,7 @@ Constants
.. versionremoved:: 10.0.0 .. versionremoved:: 10.0.0
A number of constants have been removed. A number of constants have been removed.
Instead, ``enum.IntEnum`` classes have been added. Instead, :py:class:`enum.IntEnum` classes have been added.
.. note:: .. note::
@ -338,8 +375,8 @@ ImageCms.CmsProfile attributes
.. deprecated:: 3.2.0 .. deprecated:: 3.2.0
.. versionremoved:: 8.0.0 .. versionremoved:: 8.0.0
Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed.
they issued a :py:exc:`DeprecationWarning`: From 6.0.0, they issued a :py:exc:`DeprecationWarning`:
======================== =================================================== ======================== ===================================================
Removed Use instead Removed Use instead

View File

@ -696,6 +696,25 @@ PCX
Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data. 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 PNG
^^^ ^^^

View File

@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions build status (Wheels) :alt: GitHub Actions build status (Wheels)
.. image:: https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels
:target: https://app.travis-ci.com/github/python-pillow/Pillow
:alt: Travis CI wheels build status (aarch64)
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg .. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
:target: https://app.codecov.io/gh/python-pillow/Pillow :target: https://app.codecov.io/gh/python-pillow/Pillow
:alt: Code coverage :alt: Code coverage
@ -62,11 +58,11 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:alt: Fuzzing Status :alt: Fuzzing Status
.. image:: https://img.shields.io/pypi/v/pillow.svg .. image:: https://img.shields.io/pypi/v/pillow.svg
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/pillow/
:alt: Latest PyPI version :alt: Latest PyPI version
.. image:: https://img.shields.io/pypi/dm/pillow.svg .. image:: https://img.shields.io/pypi/dm/pillow.svg
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/pillow/
:alt: Number of PyPI downloads :alt: Number of PyPI downloads
.. image:: https://www.bestpractices.dev/projects/6331/badge .. image:: https://www.bestpractices.dev/projects/6331/badge

View File

@ -385,7 +385,7 @@ After navigating to the Pillow directory, run::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install . python3 -m pip install .
.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files .. _compressed archive from PyPI: https://pypi.org/project/pillow/#files
Build Options Build Options
""""""""""""" """""""""""""
@ -602,5 +602,5 @@ Old Versions
------------ ------------
You can download old distributions from the `release history at PyPI You can download old distributions from the `release history at PyPI
<https://pypi.org/project/Pillow/#history>`_ and by direct URL access <https://pypi.org/project/pillow/#history>`_ and by direct URL access
eg. https://pypi.org/project/Pillow/1.0/. eg. https://pypi.org/project/pillow/1.0/.

View File

@ -4,8 +4,9 @@
:py:mod:`~PIL.ExifTags` Module :py:mod:`~PIL.ExifTags` Module
============================== ==============================
The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum`
which provide constants and clear-text names for various well-known EXIF tags. classes which provide constants and clear-text names for various well-known
EXIF tags.
.. py:data:: Base .. py:data:: Base

View File

@ -8,9 +8,34 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management
support using the LittleCMS2 color management engine, based on Kevin support using the LittleCMS2 color management engine, based on Kevin
Cazabon's PyCMS library. Cazabon's PyCMS library.
.. autoclass:: ImageCmsProfile
:members:
:special-members: __init__
.. autoclass:: ImageCmsTransform .. autoclass:: ImageCmsTransform
:members:
:undoc-members:
:show-inheritance:
.. autoexception:: PyCMSError .. 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 Functions
--------- ---------
@ -37,13 +62,15 @@ CmsProfile
---------- ----------
The ICC color profiles are wrapped in an instance of the class 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. information about the meaning of the values in ICC profiles.
For convenience, all XYZ-values are also given as xyY-values (so they For convenience, all XYZ-values are also given as xyY-values (so they
can be easily displayed in a chromaticity diagram, for example). can be easily displayed in a chromaticity diagram, for example).
.. py:currentmodule:: PIL.ImageCms.core
.. py:class:: CmsProfile .. py:class:: CmsProfile
:canonical: PIL._imagingcms.CmsProfile
.. py:attribute:: creation_date .. py:attribute:: creation_date
:type: Optional[datetime.datetime] :type: Optional[datetime.datetime]

View File

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

View File

@ -25,6 +25,7 @@ Reference
ImageShow ImageShow
ImageStat ImageStat
ImageTk ImageTk
ImageTransform
ImageWin ImageWin
ExifTags ExifTags
TiffTags TiffTags

View File

@ -43,7 +43,7 @@ Constants
^^^^^^^^^ ^^^^^^^^^
A number of constants have been removed. 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 Removed Use instead

View File

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

View File

@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring
ImageCms.CmsProfile attributes 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 Removed Use instead

View File

@ -51,7 +51,7 @@ Constants
^^^^^^^^^ ^^^^^^^^^
A number of constants have been deprecated and will be removed in Pillow 10.0.0 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:: .. note::

View File

@ -33,8 +33,9 @@ Added ExifTags enums
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
The data from :py:data:`~PIL.ExifTags.TAGS` and The data from :py:data:`~PIL.ExifTags.TAGS` and
:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum`` :py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as
classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. :py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and
:py:data:`~PIL.ExifTags.GPS`.
Security Security

View File

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

View File

@ -146,10 +146,7 @@ exclude = [
'^src/PIL/DdsImagePlugin.py$', '^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$', '^src/PIL/Image.py$',
'^src/PIL/ImageMath.py$',
'^src/PIL/ImageMorph.py$',
'^src/PIL/ImageQt.py$', '^src/PIL/ImageQt.py$',
'^src/PIL/ImageShow.py$',
'^src/PIL/ImImagePlugin.py$', '^src/PIL/ImImagePlugin.py$',
'^src/PIL/MicImagePlugin.py$', '^src/PIL/MicImagePlugin.py$',
'^src/PIL/PdfParser.py$', '^src/PIL/PdfParser.py$',

View File

@ -15,7 +15,7 @@ import math
from . import Image, ImageFile from . import Image, ImageFile
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:6] == b"SIMPLE" return prefix[:6] == b"SIMPLE"
@ -23,8 +23,10 @@ class FitsImageFile(ImageFile.ImageFile):
format = "FITS" format = "FITS"
format_description = "FITS" format_description = "FITS"
def _open(self): def _open(self) -> None:
headers = {} assert self.fp is not None
headers: dict[bytes, bytes] = {}
while True: while True:
header = self.fp.read(80) header = self.fp.read(80)
if not header: if not header:

View File

@ -27,6 +27,8 @@
""" """
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from . import ImageFile, ImagePalette, UnidentifiedImageError from . import ImageFile, ImagePalette, UnidentifiedImageError
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
@ -43,8 +45,10 @@ class GdImageFile(ImageFile.ImageFile):
format = "GD" format = "GD"
format_description = "GD uncompressed images" format_description = "GD uncompressed images"
def _open(self): def _open(self) -> None:
# Header # Header
assert self.fp is not None
s = self.fp.read(1037) s = self.fp.read(1037)
if i16(s) not in [65534, 65535]: if i16(s) not in [65534, 65535]:
@ -76,7 +80,7 @@ class GdImageFile(ImageFile.ImageFile):
] ]
def open(fp, mode="r"): def open(fp: BytesIO, mode: str = "r") -> GdImageFile:
""" """
Load texture from a GD image file. Load texture from a GD image file.

View File

@ -242,7 +242,7 @@ MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "
_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") _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 Gets the "base" mode for given mode. This function returns "L" for
images that contain grayscale data, and "RGB" for images that images that contain grayscale data, and "RGB" for images that
@ -282,7 +282,7 @@ def getmodebandnames(mode):
return ImageMode.getmode(mode).bands return ImageMode.getmode(mode).bands
def getmodebands(mode): def getmodebands(mode: str) -> int:
""" """
Gets the number of individual bands for this mode. Gets the number of individual bands for this mode.
@ -583,7 +583,9 @@ class Image:
else: else:
self.load() self.load()
def _dump(self, file=None, format=None, **options): def _dump(
self, file: str | None = None, format: str | None = None, **options
) -> str:
suffix = "" suffix = ""
if format: if format:
suffix = "." + format suffix = "." + format
@ -708,7 +710,7 @@ class Image:
self.putpalette(palette) self.putpalette(palette)
self.frombytes(data) 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. Return image as a bytes object.
@ -786,7 +788,7 @@ class Image:
] ]
) )
def frombytes(self, data, decoder_name="raw", *args): def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None:
""" """
Loads this image with pixel data from a bytes object. Loads this image with pixel data from a bytes object.
@ -873,7 +875,7 @@ class Image:
def convert( def convert(
self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 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 Returns a converted copy of this image. For the "P" mode, this
method translates pixels through the palette. If mode is method translates pixels through the palette. If mode is
@ -1295,7 +1297,7 @@ class Image:
] ]
return merge(self.mode, ims) 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. Returns a tuple containing the name of each band in this image.
For example, ``getbands`` on an RGB image returns ("R", "G", "B"). For example, ``getbands`` on an RGB image returns ("R", "G", "B").
@ -1305,7 +1307,7 @@ class Image:
""" """
return ImageMode.getmode(self.mode).bands 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 Calculates the bounding box of the non-zero regions in the
image. image.
@ -2493,7 +2495,7 @@ class Image:
_show(self, title=title) _show(self, title=title)
def split(self): def split(self) -> tuple[Image, ...]:
""" """
Split this image into individual bands. This method returns a Split this image into individual bands. This method returns a
tuple of individual image bands from an image. For example, tuple of individual image bands from an image. For example,
@ -2666,6 +2668,10 @@ class Image:
def transform(self, size, data, resample, fill=1): def transform(self, size, data, resample, fill=1):
# Return result # 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 It may also be an object with a ``method.getdata`` method
that returns a tuple supplying new ``method`` and ``data`` values:: that returns a tuple supplying new ``method`` and ``data`` values::
@ -3431,7 +3437,7 @@ def register_open(id, factory, accept=None) -> None:
OPEN[id] = factory, accept 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 Registers an image MIME type by populating ``Image.MIME``. This function
should not be used in application code. should not be used in application code.
@ -3446,7 +3452,7 @@ def register_mime(id, mimetype):
MIME[id.upper()] = 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 Registers an image save function. This function should not be
used in application code. used in application code.
@ -3480,7 +3486,7 @@ def register_extension(id, extension) -> None:
EXTENSION[extension.lower()] = id.upper() EXTENSION[extension.lower()] = id.upper()
def register_extensions(id, extensions): def register_extensions(id, extensions) -> None:
""" """
Registers image extensions. This function should not be Registers image extensions. This function should not be
used in application code. used in application code.
@ -3501,7 +3507,7 @@ def registered_extensions():
return EXTENSION return EXTENSION
def register_decoder(name, decoder): def register_decoder(name: str, decoder) -> None:
""" """
Registers an image decoder. This function should not be Registers an image decoder. This function should not be
used in application code. used in application code.

View File

@ -4,6 +4,9 @@
# Optional color management support, based on Kevin Cazabon's PyCMS # Optional color management support, based on Kevin Cazabon's PyCMS
# library. # library.
# Originally released under LGPL. Graciously donated to PIL in
# March 2009, for distribution under the standard PIL license
# History: # History:
# 2009-03-08 fl Added to PIL. # 2009-03-08 fl Added to PIL.
@ -16,10 +19,14 @@
# below for the original description. # below for the original description.
from __future__ import annotations from __future__ import annotations
import operator
import sys import sys
from enum import IntEnum from enum import IntEnum, IntFlag
from functools import reduce
from typing import Any
from . import Image from . import Image
from ._deprecate import deprecate
try: try:
from . import _imagingcms from . import _imagingcms
@ -30,7 +37,7 @@ except ImportError as ex:
_imagingcms = DeferredError.new(ex) _imagingcms = DeferredError.new(ex)
DESCRIPTION = """ _DESCRIPTION = """
pyCMS pyCMS
a Python / PIL interface to the littleCMS ICC Color Management System a Python / PIL interface to the littleCMS ICC Color Management System
@ -93,7 +100,22 @@ pyCMS
""" """
VERSION = "1.0.0 pil" _VERSION = "1.0.0 pil"
def __getattr__(name: str) -> Any:
if name == "DESCRIPTION":
deprecate("PIL.ImageCms.DESCRIPTION", 12)
return _DESCRIPTION
elif name == "VERSION":
deprecate("PIL.ImageCms.VERSION", 12)
return _VERSION
elif name == "FLAGS":
deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags")
return _FLAGS
msg = f"module '{__name__}' has no attribute '{name}'"
raise AttributeError(msg)
# --------------------------------------------------------------------. # --------------------------------------------------------------------.
@ -119,7 +141,70 @@ class Direction(IntEnum):
# #
# flags # flags
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, "MATRIXINPUT": 1,
"MATRIXOUTPUT": 2, "MATRIXOUTPUT": 2,
"MATRIXONLY": (1 | 2), "MATRIXONLY": (1 | 2),
@ -142,11 +227,6 @@ FLAGS = {
"GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints "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 # Experimental PIL-level API
@ -218,7 +298,7 @@ class ImageCmsTransform(Image.ImagePointHandler):
intent=Intent.PERCEPTUAL, intent=Intent.PERCEPTUAL,
proof=None, proof=None,
proof_intent=Intent.ABSOLUTE_COLORIMETRIC, proof_intent=Intent.ABSOLUTE_COLORIMETRIC,
flags=0, flags=Flags.NONE,
): ):
if proof is None: if proof is None:
self.transform = core.buildTransform( self.transform = core.buildTransform(
@ -303,7 +383,7 @@ def profileToProfile(
renderingIntent=Intent.PERCEPTUAL, renderingIntent=Intent.PERCEPTUAL,
outputMode=None, outputMode=None,
inPlace=False, inPlace=False,
flags=0, flags=Flags.NONE,
): ):
""" """
(pyCMS) Applies an ICC transformation to a given image, mapping from (pyCMS) Applies an ICC transformation to a given image, mapping from
@ -420,7 +500,7 @@ def buildTransform(
inMode, inMode,
outMode, outMode,
renderingIntent=Intent.PERCEPTUAL, renderingIntent=Intent.PERCEPTUAL,
flags=0, flags=Flags.NONE,
): ):
""" """
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
@ -482,7 +562,7 @@ def buildTransform(
raise PyCMSError(msg) raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): 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) raise PyCMSError(msg)
try: try:
@ -505,7 +585,7 @@ def buildProofTransform(
outMode, outMode,
renderingIntent=Intent.PERCEPTUAL, renderingIntent=Intent.PERCEPTUAL,
proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC,
flags=FLAGS["SOFTPROOFING"], flags=Flags.SOFTPROOFING,
): ):
""" """
(pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
@ -586,7 +666,7 @@ def buildProofTransform(
raise PyCMSError(msg) raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): 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) raise PyCMSError(msg)
try: try:
@ -1004,4 +1084,9 @@ def versions():
(pyCMS) Fetches 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__

View File

@ -514,7 +514,7 @@ class Parser:
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im, fp, tile, bufsize=0): def _save(im, fp, tile, bufsize=0) -> None:
"""Helper to save image based on tile list """Helper to save image based on tile list
:param im: Image object. :param im: Image object.
@ -616,6 +616,8 @@ class PyCodecState:
class PyCodec: class PyCodec:
fd: io.BytesIO | None
def __init__(self, mode, *args): def __init__(self, mode, *args):
self.im = None self.im = None
self.state = PyCodecState() self.state = PyCodecState()
@ -713,7 +715,7 @@ class PyDecoder(PyCodec):
msg = "unavailable in base decoder" msg = "unavailable in base decoder"
raise NotImplementedError(msg) 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 Convenience method to set the internal image from a stream of raw data

View File

@ -584,22 +584,13 @@ class FreeTypeFont:
_string_length_check(text) _string_length_check(text)
if start is None: if start is None:
start = (0, 0) start = (0, 0)
im = None
size = None
def fill(width, height): def fill(width, height):
nonlocal im, size
size = (width, height) size = (width, height)
if Image.MAX_IMAGE_PIXELS is not None: Image._decompression_bomb_check(size)
pixels = max(1, width) * max(1, height) return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
if pixels > 2 * Image.MAX_IMAGE_PIXELS:
return
im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size) return self.font.render(
return im
offset = self.font.render(
text, text,
fill, fill,
mode, mode,
@ -612,8 +603,6 @@ class FreeTypeFont:
start[0], start[0],
start[1], start[1],
) )
Image._decompression_bomb_check(size)
return im, offset
def font_variant( def font_variant(
self, font=None, size=None, index=None, encoding=None, layout_engine=None self, font=None, size=None, index=None, encoding=None, layout_engine=None

View File

@ -17,6 +17,8 @@
from __future__ import annotations from __future__ import annotations
import builtins import builtins
from types import CodeType
from typing import Any
from . import Image, _imagingmath from . import Image, _imagingmath
@ -24,10 +26,10 @@ from . import Image, _imagingmath
class _Operand: class _Operand:
"""Wraps an image operand, providing standard operators""" """Wraps an image operand, providing standard operators"""
def __init__(self, im): def __init__(self, im: Image.Image):
self.im = im self.im = im
def __fixup(self, im1): def __fixup(self, im1: _Operand | float) -> Image.Image:
# convert image to suitable mode # convert image to suitable mode
if isinstance(im1, _Operand): if isinstance(im1, _Operand):
# argument was an image. # argument was an image.
@ -45,122 +47,131 @@ class _Operand:
else: else:
return Image.new("F", self.im.size, im1) return Image.new("F", self.im.size, im1)
def apply(self, op, im1, im2=None, mode=None): def apply(
im1 = self.__fixup(im1) 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: if im2 is None:
# unary operation # unary operation
out = Image.new(mode or im1.mode, im1.size, None) out = Image.new(mode or im_1.mode, im_1.size, None)
im1.load() im_1.load()
try: try:
op = getattr(_imagingmath, op + "_" + im1.mode) op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e 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: else:
# binary operation # binary operation
im2 = self.__fixup(im2) im_2 = self.__fixup(im2)
if im1.mode != im2.mode: if im_1.mode != im_2.mode:
# convert both arguments to floating point # convert both arguments to floating point
if im1.mode != "F": if im_1.mode != "F":
im1 = im1.convert("F") im_1 = im_1.convert("F")
if im2.mode != "F": if im_2.mode != "F":
im2 = im2.convert("F") im_2 = im_2.convert("F")
if im1.size != im2.size: if im_1.size != im_2.size:
# crop both arguments to a common size # crop both arguments to a common size
size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) size = (
if im1.size != size: min(im_1.size[0], im_2.size[0]),
im1 = im1.crop((0, 0) + size) min(im_1.size[1], im_2.size[1]),
if im2.size != size: )
im2 = im2.crop((0, 0) + size) if im_1.size != size:
out = Image.new(mode or im1.mode, im1.size, None) im_1 = im_1.crop((0, 0) + size)
im1.load() if im_2.size != size:
im2.load() 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: try:
op = getattr(_imagingmath, op + "_" + im1.mode) op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e 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) return _Operand(out)
# unary operators # unary operators
def __bool__(self): def __bool__(self) -> bool:
# an image is "true" if it contains at least one non-zero pixel # an image is "true" if it contains at least one non-zero pixel
return self.im.getbbox() is not None return self.im.getbbox() is not None
def __abs__(self): def __abs__(self) -> _Operand:
return self.apply("abs", self) return self.apply("abs", self)
def __pos__(self): def __pos__(self) -> _Operand:
return self return self
def __neg__(self): def __neg__(self) -> _Operand:
return self.apply("neg", self) return self.apply("neg", self)
# binary operators # binary operators
def __add__(self, other): def __add__(self, other: _Operand | float) -> _Operand:
return self.apply("add", self, other) return self.apply("add", self, other)
def __radd__(self, other): def __radd__(self, other: _Operand | float) -> _Operand:
return self.apply("add", other, self) return self.apply("add", other, self)
def __sub__(self, other): def __sub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", self, other) return self.apply("sub", self, other)
def __rsub__(self, other): def __rsub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", other, self) return self.apply("sub", other, self)
def __mul__(self, other): def __mul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", self, other) return self.apply("mul", self, other)
def __rmul__(self, other): def __rmul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", other, self) return self.apply("mul", other, self)
def __truediv__(self, other): def __truediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", self, other) return self.apply("div", self, other)
def __rtruediv__(self, other): def __rtruediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", other, self) return self.apply("div", other, self)
def __mod__(self, other): def __mod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", self, other) return self.apply("mod", self, other)
def __rmod__(self, other): def __rmod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", other, self) return self.apply("mod", other, self)
def __pow__(self, other): def __pow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", self, other) return self.apply("pow", self, other)
def __rpow__(self, other): def __rpow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", other, self) return self.apply("pow", other, self)
# bitwise # bitwise
def __invert__(self): def __invert__(self) -> _Operand:
return self.apply("invert", self) return self.apply("invert", self)
def __and__(self, other): def __and__(self, other: _Operand | float) -> _Operand:
return self.apply("and", self, other) return self.apply("and", self, other)
def __rand__(self, other): def __rand__(self, other: _Operand | float) -> _Operand:
return self.apply("and", other, self) return self.apply("and", other, self)
def __or__(self, other): def __or__(self, other: _Operand | float) -> _Operand:
return self.apply("or", self, other) return self.apply("or", self, other)
def __ror__(self, other): def __ror__(self, other: _Operand | float) -> _Operand:
return self.apply("or", other, self) return self.apply("or", other, self)
def __xor__(self, other): def __xor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", self, other) return self.apply("xor", self, other)
def __rxor__(self, other): def __rxor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", other, self) return self.apply("xor", other, self)
def __lshift__(self, other): def __lshift__(self, other: _Operand | float) -> _Operand:
return self.apply("lshift", self, other) return self.apply("lshift", self, other)
def __rshift__(self, other): def __rshift__(self, other: _Operand | float) -> _Operand:
return self.apply("rshift", self, other) return self.apply("rshift", self, other)
# logical # logical
@ -170,56 +181,61 @@ class _Operand:
def __ne__(self, other): def __ne__(self, other):
return self.apply("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) return self.apply("lt", self, other)
def __le__(self, other): def __le__(self, other: _Operand | float) -> _Operand:
return self.apply("le", self, other) return self.apply("le", self, other)
def __gt__(self, other): def __gt__(self, other: _Operand | float) -> _Operand:
return self.apply("gt", self, other) return self.apply("gt", self, other)
def __ge__(self, other): def __ge__(self, other: _Operand | float) -> _Operand:
return self.apply("ge", self, other) return self.apply("ge", self, other)
# conversions # conversions
def imagemath_int(self): def imagemath_int(self: _Operand) -> _Operand:
return _Operand(self.im.convert("I")) return _Operand(self.im.convert("I"))
def imagemath_float(self): def imagemath_float(self: _Operand) -> _Operand:
return _Operand(self.im.convert("F")) return _Operand(self.im.convert("F"))
# logical # logical
def imagemath_equal(self, other): def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("eq", self, other, mode="I") 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") 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) 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) 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)) return _Operand(self.im.convert(mode))
ops = {} ops = {
for k, v in list(globals().items()): "int": imagemath_int,
if k[:10] == "imagemath_": "float": imagemath_float,
ops[k[10:]] = v "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. Evaluates an image expression.
@ -233,7 +249,7 @@ def eval(expression, _dict={}, **kw):
""" """
# build execution namespace # build execution namespace
args = ops.copy() args: dict[str, Any] = ops.copy()
for k in list(_dict.keys()) + list(kw.keys()): for k in list(_dict.keys()) + list(kw.keys()):
if "__" in k or hasattr(builtins, k): if "__" in k or hasattr(builtins, k):
msg = f"'{k}' not allowed" msg = f"'{k}' not allowed"
@ -247,7 +263,7 @@ def eval(expression, _dict={}, **kw):
compiled_code = compile(expression, "<string>", "eval") compiled_code = compile(expression, "<string>", "eval")
def scan(code): def scan(code: CodeType) -> None:
for const in code.co_consts: for const in code.co_consts:
if type(const) is type(compiled_code): if type(const) is type(compiled_code):
scan(const) scan(const)

View File

@ -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: if patterns is not None:
self.patterns = patterns self.patterns = patterns
else: else:
self.patterns = [] self.patterns = []
self.lut = None self.lut: bytearray | None = None
if op_name is not None: if op_name is not None:
known_patterns = { known_patterns = {
"corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"],
@ -87,25 +89,27 @@ class LutBuilder:
self.patterns = known_patterns[op_name] self.patterns = known_patterns[op_name]
def add_patterns(self, patterns): def add_patterns(self, patterns: list[str]) -> None:
self.patterns += patterns self.patterns += patterns
def build_default_lut(self): def build_default_lut(self) -> None:
symbols = [0, 1] symbols = [0, 1]
m = 1 << 4 # pos of current pixel m = 1 << 4 # pos of current pixel
self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) 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 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_permute takes a pattern and a permutation and returns the
string permuted according to the permutation list. string permuted according to the permutation list.
""" """
assert len(permutation) == 9 assert len(permutation) == 9
return "".join(pattern[p] for p in permutation) 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 """pattern_permute takes a basic pattern and its result and clones
the pattern according to the modifications described in the $options the pattern according to the modifications described in the $options
parameter. It returns a list of all cloned patterns.""" parameter. It returns a list of all cloned patterns."""
@ -135,12 +139,13 @@ class LutBuilder:
return patterns return patterns
def build_lut(self): def build_lut(self) -> bytearray:
"""Compile all patterns into a morphology lut. """Compile all patterns into a morphology lut.
TBD :Build based on (file) morphlut:modify_lut TBD :Build based on (file) morphlut:modify_lut
""" """
self.build_default_lut() self.build_default_lut()
assert self.lut is not None
patterns = [] patterns = []
# Parse and create symmetries of the patterns strings # Parse and create symmetries of the patterns strings
@ -159,10 +164,10 @@ class LutBuilder:
patterns += self._pattern_permute(pattern, options, result) patterns += self._pattern_permute(pattern, options, result)
# compile the patterns into regular expressions for speed # 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 = pattern[0].replace(".", "X").replace("X", "[01]")
p = re.compile(p) compiled_patterns.append((re.compile(p), pattern[1]))
patterns[i] = (p, pattern[1])
# Step through table and find patterns that match. # Step through table and find patterns that match.
# Note that all the patterns are searched. The last one # Note that all the patterns are searched. The last one
@ -172,8 +177,8 @@ class LutBuilder:
bitpattern = bin(i)[2:] bitpattern = bin(i)[2:]
bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1]
for p, r in patterns: for pattern, r in compiled_patterns:
if p.match(bitpattern): if pattern.match(bitpattern):
self.lut[i] = [0, 1][r] self.lut[i] = [0, 1][r]
return self.lut return self.lut
@ -182,7 +187,12 @@ class LutBuilder:
class MorphOp: class MorphOp:
"""A class for binary morphological operators""" """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""" """Create a binary morphological operator"""
self.lut = lut self.lut = lut
if op_name is not None: if op_name is not None:
@ -190,7 +200,7 @@ class MorphOp:
elif patterns is not None: elif patterns is not None:
self.lut = LutBuilder(patterns=patterns).build_lut() 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 """Run a single morphological operation on an image
Returns a tuple of the number of changed pixels and the Returns a tuple of the number of changed pixels and the
@ -206,7 +216,7 @@ class MorphOp:
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage return count, outimage
def match(self, image): def match(self, image: Image.Image):
"""Get a list of coordinates matching the morphological operation on """Get a list of coordinates matching the morphological operation on
an image. an image.
@ -221,7 +231,7 @@ class MorphOp:
raise ValueError(msg) raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id) 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 """Get a list of all turned on pixels in a binary image
Returns a list of tuples of (x,y) coordinates Returns a list of tuples of (x,y) coordinates
@ -232,7 +242,7 @@ class MorphOp:
raise ValueError(msg) raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.im.id) 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""" """Load an operator from an mrl file"""
with open(filename, "rb") as f: with open(filename, "rb") as f:
self.lut = bytearray(f.read()) self.lut = bytearray(f.read())
@ -242,7 +252,7 @@ class MorphOp:
msg = "Wrong size operator file!" msg = "Wrong size operator file!"
raise Exception(msg) raise Exception(msg)
def save_lut(self, filename): def save_lut(self, filename: str) -> None:
"""Save an operator to an mrl file""" """Save an operator to an mrl file"""
if self.lut is None: if self.lut is None:
msg = "No operator loaded" msg = "No operator loaded"
@ -250,6 +260,6 @@ class MorphOp:
with open(filename, "wb") as f: with open(filename, "wb") as f:
f.write(self.lut) f.write(self.lut)
def set_lut(self, lut): def set_lut(self, lut: bytearray | None) -> None:
"""Set the lut from an external source""" """Set the lut from an external source"""
self.lut = lut self.lut = lut

View File

@ -192,7 +192,7 @@ class ImagePalette:
# Internal # Internal
def raw(rawmode, data): def raw(rawmode, data) -> ImagePalette:
palette = ImagePalette() palette = ImagePalette()
palette.rawmode = rawmode palette.rawmode = rawmode
palette.palette = data palette.palette = data

View File

@ -13,18 +13,20 @@
# #
from __future__ import annotations from __future__ import annotations
import abc
import os import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
from shlex import quote from shlex import quote
from typing import Any
from . import Image from . import Image
_viewers = [] _viewers = []
def register(viewer, order=1): def register(viewer, order: int = 1) -> None:
""" """
The :py:func:`register` function is used to register additional viewers:: The :py:func:`register` function is used to register additional viewers::
@ -49,7 +51,7 @@ def register(viewer, order=1):
_viewers.insert(0, viewer) _viewers.insert(0, viewer)
def show(image, title=None, **options): def show(image: Image.Image, title: str | None = None, **options: Any) -> bool:
r""" r"""
Display a given image. Display a given image.
@ -69,7 +71,7 @@ class Viewer:
# main api # main api
def show(self, image, **options): def show(self, image: Image.Image, **options: Any) -> int:
""" """
The main function for displaying an image. The main function for displaying an image.
Converts the given image to the target format and displays it. Converts the given image to the target format and displays it.
@ -87,16 +89,16 @@ class Viewer:
# hook methods # hook methods
format = None format: str | None = None
"""The format to convert the image into.""" """The format to convert the image into."""
options = {} options: dict[str, Any] = {}
"""Additional options used to convert the image.""" """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 format name, or ``None`` to save as PGM/PPM."""
return self.format 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. Returns the command used to display the file.
Not implemented in the base class. Not implemented in the base class.
@ -104,15 +106,15 @@ class Viewer:
msg = "unavailable in base viewer" msg = "unavailable in base viewer"
raise NotImplementedError(msg) raise NotImplementedError(msg)
def save_image(self, image): def save_image(self, image: Image.Image) -> str:
"""Save to temporary file and return filename.""" """Save to temporary file and return filename."""
return image._dump(format=self.get_format(image), **self.options) 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.""" """Display the given image."""
return self.show_file(self.save_image(image), **options) 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. Display given file.
""" """
@ -129,7 +131,7 @@ class WindowsViewer(Viewer):
format = "PNG" format = "PNG"
options = {"compress_level": 1, "save_all": True} options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options): def get_command(self, file: str, **options: Any) -> str:
return ( return (
f'start "Pillow" /WAIT "{file}" ' f'start "Pillow" /WAIT "{file}" '
"&& ping -n 4 127.0.0.1 >NUL " "&& ping -n 4 127.0.0.1 >NUL "
@ -147,14 +149,14 @@ class MacViewer(Viewer):
format = "PNG" format = "PNG"
options = {"compress_level": 1, "save_all": True} 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 # on darwin open returns immediately resulting in the temp
# file removal while app is opening # file removal while app is opening
command = "open -a Preview.app" command = "open -a Preview.app"
command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
return command return command
def show_file(self, path, **options): def show_file(self, path: str, **options: Any) -> int:
""" """
Display given file. Display given file.
""" """
@ -180,7 +182,11 @@ class UnixViewer(Viewer):
format = "PNG" format = "PNG"
options = {"compress_level": 1, "save_all": True} 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] command = self.get_command_ex(file, **options)[0]
return f"({command} {quote(file)}" return f"({command} {quote(file)}"
@ -190,11 +196,11 @@ class XDGViewer(UnixViewer):
The freedesktop.org ``xdg-open`` command. 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" command = executable = "xdg-open"
return command, executable return command, executable
def show_file(self, path, **options): def show_file(self, path: str, **options: Any) -> int:
""" """
Display given file. Display given file.
""" """
@ -208,13 +214,15 @@ class DisplayViewer(UnixViewer):
This viewer supports the ``title`` parameter. 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" command = executable = "display"
if title: if title:
command += f" -title {quote(title)}" command += f" -title {quote(title)}"
return command, executable return command, executable
def show_file(self, path, **options): def show_file(self, path: str, **options: Any) -> int:
""" """
Display given file. Display given file.
""" """
@ -231,12 +239,12 @@ class DisplayViewer(UnixViewer):
class GmDisplayViewer(UnixViewer): class GmDisplayViewer(UnixViewer):
"""The GraphicsMagick ``gm display`` command.""" """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" executable = "gm"
command = "gm display" command = "gm display"
return command, executable return command, executable
def show_file(self, path, **options): def show_file(self, path: str, **options: Any) -> int:
""" """
Display given file. Display given file.
""" """
@ -247,12 +255,12 @@ class GmDisplayViewer(UnixViewer):
class EogViewer(UnixViewer): class EogViewer(UnixViewer):
"""The GNOME Image Viewer ``eog`` command.""" """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" executable = "eog"
command = "eog -n" command = "eog -n"
return command, executable return command, executable
def show_file(self, path, **options): def show_file(self, path: str, **options: Any) -> int:
""" """
Display given file. Display given file.
""" """
@ -266,7 +274,9 @@ class XVViewer(UnixViewer):
This viewer supports the ``title`` parameter. 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 # note: xv is pretty outdated. most modern systems have
# imagemagick's display command instead. # imagemagick's display command instead.
command = executable = "xv" command = executable = "xv"
@ -274,7 +284,7 @@ class XVViewer(UnixViewer):
command += f" -name {quote(title)}" command += f" -name {quote(title)}"
return command, executable return command, executable
def show_file(self, path, **options): def show_file(self, path: str, **options: Any) -> int:
""" """
Display given file. Display given file.
""" """
@ -304,7 +314,7 @@ if sys.platform not in ("win32", "darwin"): # unixoids
class IPythonViewer(Viewer): class IPythonViewer(Viewer):
"""The viewer for IPython frontends.""" """The viewer for IPython frontends."""
def show_image(self, image, **options): def show_image(self, image: Image.Image, **options: Any) -> int:
ipython_display(image) ipython_display(image)
return 1 return 1

View File

@ -20,12 +20,14 @@ from . import Image
class Transform(Image.ImageTransformHandler): class Transform(Image.ImageTransformHandler):
"""Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`."""
method: Image.Transform method: Image.Transform
def __init__(self, data: Sequence[int]) -> None: def __init__(self, data: Sequence[int]) -> None:
self.data = data self.data = data
def getdata(self) -> tuple[int, Sequence[int]]: def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
return self.method, self.data return self.method, self.data
def transform( def transform(
@ -34,6 +36,7 @@ class Transform(Image.ImageTransformHandler):
image: Image.Image, image: Image.Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]], **options: dict[str, str | int | tuple[int, ...] | list[int]],
) -> Image.Image: ) -> Image.Image:
"""Perform the transform. Called from :py:meth:`.Image.transform`."""
# can be overridden # can be overridden
method, data = self.getdata() method, data = self.getdata()
return image.transform(size, method, data, **options) return image.transform(size, method, data, **options)
@ -51,7 +54,7 @@ class AffineTransform(Transform):
This function can be used to scale, translate, rotate, and shear the This function can be used to scale, translate, rotate, and shear the
original image. 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 :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
from an affine transform matrix. from an affine transform matrix.
@ -60,6 +63,26 @@ class AffineTransform(Transform):
method = Image.Transform.AFFINE 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): class ExtentTransform(Transform):
""" """
Define a transform to extract a subregion from an image. Define a transform to extract a subregion from an image.
@ -73,7 +96,7 @@ class ExtentTransform(Transform):
rectangle in the current image. It is slightly slower than crop, but about rectangle in the current image. It is slightly slower than crop, but about
as fast as a corresponding resize operation. 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 :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the
input image's coordinate system. See :ref:`coordinate-system`. input image's coordinate system. See :ref:`coordinate-system`.
@ -89,7 +112,7 @@ class QuadTransform(Transform):
Maps a quadrilateral (a region defined by four corners) from the image to a Maps a quadrilateral (a region defined by four corners) from the image to a
rectangle of the given size. 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 :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 upper left, lower left, lower right, and upper right corner of the
@ -104,7 +127,7 @@ class MeshTransform(Transform):
Define a mesh image transform. A mesh transform consists of one or more Define a mesh image transform. A mesh transform consists of one or more
individual quad transforms. individual quad transforms.
See :py:meth:`~PIL.Image.Image.transform` See :py:meth:`.Image.transform`
:param data: A list of (bbox, quad) tuples. :param data: A list of (bbox, quad) tuples.
""" """

View File

@ -33,10 +33,12 @@ class ImtImageFile(ImageFile.ImageFile):
format = "IMT" format = "IMT"
format_description = "IM Tools" format_description = "IM Tools"
def _open(self): def _open(self) -> None:
# Quick rejection: if there's not a LF among the first # Quick rejection: if there's not a LF among the first
# 100 bytes, this is (probably) not a text header. # 100 bytes, this is (probably) not a text header.
assert self.fp is not None
buffer = self.fp.read(100) buffer = self.fp.read(100)
if b"\n" not in buffer: if b"\n" not in buffer:
msg = "not an IM file" msg = "not an IM file"

View File

@ -22,8 +22,8 @@ import struct
from . import Image, ImageFile from . import Image, ImageFile
def _accept(s): def _accept(prefix: bytes) -> bool:
return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04"
## ##
@ -34,8 +34,10 @@ class McIdasImageFile(ImageFile.ImageFile):
format = "MCIDAS" format = "MCIDAS"
format_description = "McIdas area file" format_description = "McIdas area file"
def _open(self): def _open(self) -> None:
# parse area file directory # parse area file directory
assert self.fp is not None
s = self.fp.read(256) s = self.fp.read(256)
if not _accept(s) or len(s) != 256: if not _accept(s) or len(s) != 256:
msg = "not an McIdas area file" msg = "not an McIdas area file"

View File

@ -14,6 +14,8 @@
# #
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i8 from ._binary import i8
@ -22,15 +24,15 @@ from ._binary import i8
class BitStream: class BitStream:
def __init__(self, fp): def __init__(self, fp: BytesIO) -> None:
self.fp = fp self.fp = fp
self.bits = 0 self.bits = 0
self.bitbuffer = 0 self.bitbuffer = 0
def next(self): def next(self) -> int:
return i8(self.fp.read(1)) return i8(self.fp.read(1))
def peek(self, bits): def peek(self, bits: int) -> int:
while self.bits < bits: while self.bits < bits:
c = self.next() c = self.next()
if c < 0: if c < 0:
@ -40,13 +42,13 @@ class BitStream:
self.bits += 8 self.bits += 8
return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1
def skip(self, bits): def skip(self, bits: int) -> None:
while self.bits < bits: while self.bits < bits:
self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1))
self.bits += 8 self.bits += 8
self.bits = self.bits - bits self.bits = self.bits - bits
def read(self, bits): def read(self, bits: int) -> int:
v = self.peek(bits) v = self.peek(bits)
self.bits = self.bits - bits self.bits = self.bits - bits
return v return v
@ -61,9 +63,10 @@ class MpegImageFile(ImageFile.ImageFile):
format = "MPEG" format = "MPEG"
format_description = "MPEG" format_description = "MPEG"
def _open(self): def _open(self) -> None:
s = BitStream(self.fp) assert self.fp is not None
s = BitStream(self.fp)
if s.read(32) != 0x1B3: if s.read(32) != 0x1B3:
msg = "not an MPEG file" msg = "not an MPEG file"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

@ -35,7 +35,7 @@ from ._binary import o16le as o16
# read MSP files # read MSP files
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] in [b"DanM", b"LinS"] return prefix[:4] in [b"DanM", b"LinS"]
@ -48,8 +48,10 @@ class MspImageFile(ImageFile.ImageFile):
format = "MSP" format = "MSP"
format_description = "Windows Paint" format_description = "Windows Paint"
def _open(self): def _open(self) -> None:
# Header # Header
assert self.fp is not None
s = self.fp.read(32) s = self.fp.read(32)
if not _accept(s): if not _accept(s):
msg = "not an MSP file" msg = "not an MSP file"
@ -109,7 +111,9 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
img = io.BytesIO() img = io.BytesIO()
blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8))
try: try:
@ -159,7 +163,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only) # 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": if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP" msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg) raise OSError(msg)

View File

@ -27,8 +27,10 @@ class PcdImageFile(ImageFile.ImageFile):
format = "PCD" format = "PCD"
format_description = "Kodak PhotoCD" format_description = "Kodak PhotoCD"
def _open(self): def _open(self) -> None:
# rough # rough
assert self.fp is not None
self.fp.seek(2048) self.fp.seek(2048)
s = self.fp.read(2048) s = self.fp.read(2048)
@ -47,9 +49,11 @@ class PcdImageFile(ImageFile.ImageFile):
self._size = 768, 512 # FIXME: not correct for rotated images! self._size = 768, 512 # FIXME: not correct for rotated images!
self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)]
def load_end(self): def load_end(self) -> None:
if self.tile_post_rotate: if self.tile_post_rotate:
# Handle rotated PCDs # Handle rotated PCDs
assert self.im is not None
self.im = self.im.rotate(self.tile_post_rotate) self.im = self.im.rotate(self.tile_post_rotate)
self._size = self.im.size self._size = self.im.size

View File

@ -37,7 +37,7 @@ from ._binary import o16le as o16
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
@ -49,8 +49,10 @@ class PcxImageFile(ImageFile.ImageFile):
format = "PCX" format = "PCX"
format_description = "Paintbrush" format_description = "Paintbrush"
def _open(self): def _open(self) -> None:
# header # header
assert self.fp is not None
s = self.fp.read(128) s = self.fp.read(128)
if not _accept(s): if not _accept(s):
msg = "not a PCX file" msg = "not a PCX file"
@ -141,7 +143,7 @@ SAVE = {
} }
def _save(im, fp, filename): def _save(im: Image.Image, fp: io.BytesIO, filename: str) -> None:
try: try:
version, bits, planes, rawmode = SAVE[im.mode] version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e: except KeyError as e:
@ -199,6 +201,8 @@ def _save(im, fp, filename):
if im.mode == "P": if im.mode == "P":
# colour palette # colour palette
assert im.im is not None
fp.write(o8(12)) fp.write(o8(12))
palette = im.im.getpalette("RGB", "RGB") palette = im.im.getpalette("RGB", "RGB")
palette += b"\x00" * (768 - len(palette)) palette += b"\x00" * (768 - len(palette))

View File

@ -27,7 +27,7 @@ from ._binary import i16le as i16
# helpers # helpers
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\200\350\000\000" return prefix[:4] == b"\200\350\000\000"
@ -39,8 +39,10 @@ class PixarImageFile(ImageFile.ImageFile):
format = "PIXAR" format = "PIXAR"
format_description = "PIXAR raster image" format_description = "PIXAR raster image"
def _open(self): def _open(self) -> None:
# assuming a 4-byte magic label # assuming a 4-byte magic label
assert self.fp is not None
s = self.fp.read(4) s = self.fp.read(4)
if not _accept(s): if not _accept(s):
msg = "not a PIXAR file" msg = "not a PIXAR file"

View File

@ -378,7 +378,7 @@ class PngStream(ChunkStream):
} }
def 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.im_tile = self.rewind_state["tile"]
self._seq_num = self.rewind_state["seq_num"] self._seq_num = self.rewind_state["seq_num"]

View File

@ -15,6 +15,9 @@
# #
from __future__ import annotations from __future__ import annotations
import math
from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import o8 from ._binary import o8
@ -35,6 +38,7 @@ MODES = {
b"P6": "RGB", b"P6": "RGB",
# extensions # extensions
b"P0CMYK": "CMYK", b"P0CMYK": "CMYK",
b"Pf": "F",
# PIL extensions (for test purposes only) # PIL extensions (for test purposes only)
b"PyP": "P", b"PyP": "P",
b"PyRGBA": "RGBA", b"PyRGBA": "RGBA",
@ -42,8 +46,8 @@ MODES = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[0:1] == b"P" and prefix[1] in b"0123456y" return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"
## ##
@ -54,7 +58,9 @@ class PpmImageFile(ImageFile.ImageFile):
format = "PPM" format = "PPM"
format_description = "Pbmplus image" format_description = "Pbmplus image"
def _read_magic(self): def _read_magic(self) -> bytes:
assert self.fp is not None
magic = b"" magic = b""
# read until whitespace or longest available magic number # read until whitespace or longest available magic number
for _ in range(6): for _ in range(6):
@ -64,7 +70,9 @@ class PpmImageFile(ImageFile.ImageFile):
magic += c magic += c
return magic return magic
def _read_token(self): def _read_token(self) -> bytes:
assert self.fp is not None
token = b"" token = b""
while len(token) <= 10: # read until next whitespace or limit of 10 characters while len(token) <= 10: # read until next whitespace or limit of 10 characters
c = self.fp.read(1) c = self.fp.read(1)
@ -90,13 +98,16 @@ class PpmImageFile(ImageFile.ImageFile):
raise ValueError(msg) raise ValueError(msg)
return token return token
def _open(self): def _open(self) -> None:
assert self.fp is not None
magic_number = self._read_magic() magic_number = self._read_magic()
try: try:
mode = MODES[magic_number] mode = MODES[magic_number]
except KeyError: except KeyError:
msg = "not a PPM file" msg = "not a PPM file"
raise SyntaxError(msg) raise SyntaxError(msg)
self._mode = mode
if magic_number in (b"P1", b"P4"): if magic_number in (b"P1", b"P4"):
self.custom_mimetype = "image/x-portable-bitmap" self.custom_mimetype = "image/x-portable-bitmap"
@ -105,40 +116,42 @@ class PpmImageFile(ImageFile.ImageFile):
elif magic_number in (b"P3", b"P6"): elif magic_number in (b"P3", b"P6"):
self.custom_mimetype = "image/x-portable-pixmap" self.custom_mimetype = "image/x-portable-pixmap"
maxval = None self._size = int(self._read_token()), int(self._read_token())
decoder_name = "raw" decoder_name = "raw"
if magic_number in (b"P1", b"P2", b"P3"): if magic_number in (b"P1", b"P2", b"P3"):
decoder_name = "ppm_plain" 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": args: str | tuple[str | int, ...]
# If maxval matches a bit depth, use the raw decoder directly if mode == "1":
if maxval == 65535 and mode == "L": args = "1;I"
rawmode = "I;16B" elif mode == "F":
elif maxval != 255: scale = float(self._read_token())
decoder_name = "ppm" if scale == 0.0 or not math.isfinite(scale):
msg = "scale must be finite and non-zero"
raise ValueError(msg)
self.info["scale"] = abs(scale)
args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) rawmode = "F;32F" if scale < 0 else "F;32BF"
self._size = xsize, ysize args = (rawmode, 0, -1)
self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] else:
maxval = int(self._read_token())
if not 0 < maxval < 65536:
msg = "maxval must be greater than 0 and less than 65536"
raise ValueError(msg)
if maxval > 255 and mode == "L":
self._mode = "I"
rawmode = mode
if decoder_name != "ppm_plain":
# If maxval matches a bit depth, use the raw decoder directly
if maxval == 65535 and mode == "L":
rawmode = "I;16B"
elif maxval != 255:
decoder_name = "ppm"
args = rawmode if decoder_name == "raw" else (rawmode, maxval)
self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]
# #
@ -147,16 +160,19 @@ class PpmImageFile(ImageFile.ImageFile):
class PpmPlainDecoder(ImageFile.PyDecoder): class PpmPlainDecoder(ImageFile.PyDecoder):
_pulls_fd = True _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) 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) a = block.find(b"\n", start)
b = block.find(b"\r", start) b = block.find(b"\r", start)
return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) 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: if self._comment_spans:
# Finish current comment # Finish current comment
while block: while block:
@ -190,7 +206,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
break break
return block 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 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. exactly one byte, so the inter-token whitespace is optional.
@ -215,7 +231,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
invert = bytes.maketrans(b"01", b"\xFF\x00") invert = bytes.maketrans(b"01", b"\xFF\x00")
return data.translate(invert) return data.translate(invert)
def _decode_blocks(self, maxval): def _decode_blocks(self, maxval: int) -> bytearray:
data = bytearray() data = bytearray()
max_len = 10 max_len = 10
out_byte_count = 4 if self.mode == "I" else 1 out_byte_count = 4 if self.mode == "I" else 1
@ -223,7 +239,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
bands = Image.getmodebands(self.mode) bands = Image.getmodebands(self.mode)
total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
half_token = False half_token = b""
while len(data) != total_bytes: while len(data) != total_bytes:
block = self._read_block() # read next block block = self._read_block() # read next block
if not block: if not block:
@ -237,7 +253,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
if half_token: if half_token:
block = half_token + block # stitch half_token to new block block = half_token + block # stitch half_token to new block
half_token = False half_token = b""
tokens = block.split() tokens = block.split()
@ -255,15 +271,15 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
raise ValueError(msg) raise ValueError(msg)
value = int(token) value = int(token)
if value > maxval: if value > maxval:
msg = f"Channel value too large for this mode: {value}" msg_str = f"Channel value too large for this mode: {value}"
raise ValueError(msg) raise ValueError(msg_str)
value = round(value / maxval * out_max) value = round(value / maxval * out_max)
data += o32(value) if self.mode == "I" else o8(value) data += o32(value) if self.mode == "I" else o8(value)
if len(data) == total_bytes: # finished! if len(data) == total_bytes: # finished!
break break
return data return data
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
self._comment_spans = False self._comment_spans = False
if self.mode == "1": if self.mode == "1":
data = self._decode_bitonal() data = self._decode_bitonal()
@ -279,7 +295,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
class PpmDecoder(ImageFile.PyDecoder): class PpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
data = bytearray() data = bytearray()
maxval = self.args[-1] maxval = self.args[-1]
in_byte_count = 1 if maxval < 256 else 2 in_byte_count = 1 if maxval < 256 else 2
@ -306,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im, fp, filename): def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
if im.mode == "1": if im.mode == "1":
rawmode, head = "1;I", b"P4" rawmode, head = "1;I", b"P4"
elif im.mode == "L": elif im.mode == "L":
@ -315,6 +333,8 @@ def _save(im, fp, filename):
rawmode, head = "I;16B", b"P5" rawmode, head = "I;16B", b"P5"
elif im.mode in ("RGB", "RGBA"): elif im.mode in ("RGB", "RGBA"):
rawmode, head = "RGB", b"P6" rawmode, head = "RGB", b"P6"
elif im.mode == "F":
rawmode, head = "F;32F", b"Pf"
else: else:
msg = f"cannot write mode {im.mode} as PPM" msg = f"cannot write mode {im.mode} as PPM"
raise OSError(msg) raise OSError(msg)
@ -326,7 +346,10 @@ def _save(im, fp, filename):
fp.write(b"255\n") fp.write(b"255\n")
else: else:
fp.write(b"65535\n") fp.write(b"65535\n")
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) elif head == b"Pf":
fp.write(b"-1.0\n")
row_order = -1 if im.mode == "F" else 1
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])
# #
@ -339,6 +362,6 @@ Image.register_save(PpmImageFile.format, _save)
Image.register_decoder("ppm", PpmDecoder) Image.register_decoder("ppm", PpmDecoder)
Image.register_decoder("ppm_plain", PpmPlainDecoder) 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") Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")

View File

@ -24,13 +24,14 @@ from __future__ import annotations
import os import os
import struct import struct
from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import o8 from ._binary import o8
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return len(prefix) >= 2 and i16(prefix) == 474 return len(prefix) >= 2 and i16(prefix) == 474
@ -52,8 +53,10 @@ class SgiImageFile(ImageFile.ImageFile):
format = "SGI" format = "SGI"
format_description = "SGI Image File Format" format_description = "SGI Image File Format"
def _open(self): def _open(self) -> None:
# HEAD # HEAD
assert self.fp is not None
headlen = 512 headlen = 512
s = self.fp.read(headlen) s = self.fp.read(headlen)
@ -122,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile):
] ]
def _save(im, fp, filename): def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
if im.mode not in {"RGB", "RGBA", "L"}: if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode" msg = "Unsupported SGI image mode"
raise ValueError(msg) raise ValueError(msg)
@ -168,8 +171,8 @@ def _save(im, fp, filename):
# Maximum Byte value (255 = 8bits per pixel) # Maximum Byte value (255 = 8bits per pixel)
pinmax = 255 pinmax = 255
# Image name (79 characters max, truncated below in write) # Image name (79 characters max, truncated below in write)
img_name = os.path.splitext(os.path.basename(filename))[0] filename = os.path.basename(filename)
img_name = img_name.encode("ascii", "ignore") img_name = os.path.splitext(filename)[0].encode("ascii", "ignore")
# Standard representation of pixel in the file # Standard representation of pixel in the file
colormap = 0 colormap = 0
fp.write(struct.pack(">h", magic_number)) fp.write(struct.pack(">h", magic_number))
@ -201,7 +204,10 @@ def _save(im, fp, filename):
class SGI16Decoder(ImageFile.PyDecoder): class SGI16Decoder(ImageFile.PyDecoder):
_pulls_fd = True _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 rawmode, stride, orientation = self.args
pagesize = self.state.xsize * self.state.ysize pagesize = self.state.xsize * self.state.ysize
zsize = len(self.mode) zsize = len(self.mode)

View File

@ -21,7 +21,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i32be as i32 from ._binary import i32be as i32
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 return len(prefix) >= 4 and i32(prefix) == 0x59A66A95
@ -33,7 +33,7 @@ class SunImageFile(ImageFile.ImageFile):
format = "SUN" format = "SUN"
format_description = "Sun Raster File" format_description = "Sun Raster File"
def _open(self): def _open(self) -> None:
# The Sun Raster file header is 32 bytes in length # The Sun Raster file header is 32 bytes in length
# and has the following format: # and has the following format:
@ -49,6 +49,8 @@ class SunImageFile(ImageFile.ImageFile):
# DWORD ColorMapLength; /* Size of the color map in bytes */ # DWORD ColorMapLength; /* Size of the color map in bytes */
# } SUNRASTER; # } SUNRASTER;
assert self.fp is not None
# HEAD # HEAD
s = self.fp.read(32) s = self.fp.read(32)
if not _accept(s): if not _accept(s):

View File

@ -18,6 +18,7 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from io import BytesIO
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16 from ._binary import i16le as i16
@ -49,8 +50,10 @@ class TgaImageFile(ImageFile.ImageFile):
format = "TGA" format = "TGA"
format_description = "Targa" format_description = "Targa"
def _open(self): def _open(self) -> None:
# process header # process header
assert self.fp is not None
s = self.fp.read(18) s = self.fp.read(18)
id_len = s[0] id_len = s[0]
@ -151,8 +154,9 @@ class TgaImageFile(ImageFile.ImageFile):
except KeyError: except KeyError:
pass # cannot decode pass # cannot decode
def load_end(self): def load_end(self) -> None:
if self._flip_horizontally: if self._flip_horizontally:
assert self.im is not None
self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
@ -171,7 +175,7 @@ SAVE = {
} }
def _save(im, fp, filename): def _save(im: Image.Image, fp: BytesIO, filename: str) -> None:
try: try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode] rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e: except KeyError as e:
@ -194,6 +198,7 @@ def _save(im, fp, filename):
warnings.warn("id_section has been trimmed to 255 characters") warnings.warn("id_section has been trimmed to 255 characters")
if colormaptype: if colormaptype:
assert im.im is not None
palette = im.im.getpalette("RGB", "BGR") palette = im.im.getpalette("RGB", "BGR")
colormaplength, colormapentry = len(palette) // 3, 24 colormaplength, colormapentry = len(palette) // 3, 24
else: else:

View File

@ -33,7 +33,7 @@ for r in range(8):
) )
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:6] == _MAGIC return prefix[:6] == _MAGIC
@ -45,8 +45,10 @@ class XVThumbImageFile(ImageFile.ImageFile):
format = "XVThumb" format = "XVThumb"
format_description = "XV thumbnail image" format_description = "XV thumbnail image"
def _open(self): def _open(self) -> None:
# check magic # check magic
assert self.fp is not None
if not _accept(self.fp.read(6)): if not _accept(self.fp.read(6)):
msg = "not an XV thumbnail file" msg = "not an XV thumbnail file"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

@ -21,6 +21,7 @@
from __future__ import annotations from __future__ import annotations
import re import re
from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
@ -36,7 +37,7 @@ xbm_head = re.compile(
) )
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix.lstrip()[:7] == b"#define" return prefix.lstrip()[:7] == b"#define"
@ -48,7 +49,9 @@ class XbmImageFile(ImageFile.ImageFile):
format = "XBM" format = "XBM"
format_description = "X11 Bitmap" 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)) m = xbm_head.match(self.fp.read(512))
if not m: if not m:
@ -67,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] 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": if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM" msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg) raise OSError(msg)

5
src/PIL/_imagingmath.pyi Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) {
image = PyObject_CallFunction(fill, "ii", width, height); image = PyObject_CallFunction(fill, "ii", width, height);
if (image == Py_None) { if (image == Py_None) {
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
return Py_BuildValue("ii", 0, 0); return Py_BuildValue("N(ii)", image, 0, 0);
} else if (image == NULL) { } else if (image == NULL) {
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
return NULL; return NULL;
@ -894,7 +894,7 @@ font_render(FontObject *self, PyObject *args) {
y_offset -= stroke_width; y_offset -= stroke_width;
if (count == 0 || width == 0 || height == 0) { if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info); 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) { if (stroke_width) {
@ -1130,18 +1130,12 @@ font_render(FontObject *self, PyObject *args) {
if (bitmap_converted_ready) { if (bitmap_converted_ready) {
FT_Bitmap_Done(library, &bitmap_converted); FT_Bitmap_Done(library, &bitmap_converted);
} }
Py_DECREF(image);
FT_Stroker_Done(stroker); FT_Stroker_Done(stroker);
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
return Py_BuildValue("ii", x_offset, y_offset); return Py_BuildValue("N(ii)", image, x_offset, y_offset);
glyph_error: glyph_error:
if (im->destroy) { Py_DECREF(image);
im->destroy(im);
}
if (im->image) {
free(im->image);
}
if (stroker != NULL) { if (stroker != NULL) {
FT_Done_Glyph(glyph); FT_Done_Glyph(glyph);
} }

View File

@ -105,7 +105,7 @@ encode_loop:
st->head = st->codes[st->probe] >> 20; st->head = st->codes[st->probe] >> 20;
goto encode_loop; goto encode_loop;
} else { } 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. */ * size. So, any odd positive number for power-of-2 size. */
if ((st->probe -= ((st->tail << 2) | 1)) < 0) { if ((st->probe -= ((st->tail << 2) | 1)) < 0) {
st->probe += TABLE_SIZE; st->probe += TABLE_SIZE;

View File

@ -74,7 +74,7 @@ typedef struct {
/* Optimize Huffman tables (slow) */ /* Optimize Huffman tables (slow) */
int optimize; int optimize;
/* Disable automatic conversion of RGB images to YCbCr if nonzero */ /* Disable automatic conversion of RGB images to YCbCr if non-zero */
int keep_rgb; int keep_rgb;
/* Stream type (0=full, 1=tables only, 2=image only) */ /* Stream type (0=full, 1=tables only, 2=image only) */

View File

@ -33,6 +33,7 @@ commands =
[testenv:mypy] [testenv:mypy]
skip_install = true skip_install = true
deps = deps =
ipython
mypy==1.7.1 mypy==1.7.1
numpy numpy
extras = extras =

View File

@ -27,7 +27,7 @@ Download and install:
* `Ninja <https://ninja-build.org/>`_ * `Ninja <https://ninja-build.org/>`_
(optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component)
* x86/x64: `Netwide Assembler (NASM) <https://www.nasm.us/pub/nasm/releasebuilds/?C=M;O=D>`_ * x86/AMD64: `Netwide Assembler (NASM) <https://www.nasm.us/pub/nasm/releasebuilds/?C=M;O=D>`_
Any version of Visual Studio 2017 or newer should be supported, Any version of Visual Studio 2017 or newer should be supported,
including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. 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] usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD]
[--depends PILLOW_DEPS] [--depends PILLOW_DEPS]
[--architecture {x86,x64,ARM64}] [--nmake] [--architecture {x86,AMD64,ARM64}] [--nmake]
[--no-imagequant] [--no-fribidi] [--no-imagequant] [--no-fribidi]
Download and generate build scripts for Pillow dependencies. Download and generate build scripts for Pillow dependencies.
@ -55,7 +55,7 @@ Run ``build_prepare.py`` to configure the build::
--depends PILLOW_DEPS --depends PILLOW_DEPS
directory used to store cached dependencies (default: directory used to store cached dependencies (default:
'winbuild\depends') 'winbuild\depends')
--architecture {x86,x64,ARM64} --architecture {x86,AMD64,ARM64}
build architecture (default: same as host Python) build architecture (default: same as host Python)
--nmake build dependencies using NMake instead of Ninja --nmake build dependencies using NMake instead of Ninja
--no-imagequant skip GPL-licensed optional dependency libimagequant --no-imagequant skip GPL-licensed optional dependency libimagequant

View File

@ -105,7 +105,7 @@ SF_PROJECTS = "https://sourceforge.net/projects"
ARCHITECTURES = { ARCHITECTURES = {
"x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, "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"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"},
} }
@ -174,23 +174,22 @@ DEPS = {
"filename": "libwebp-1.3.2.tar.gz", "filename": "libwebp-1.3.2.tar.gz",
"dir": "libwebp-1.3.2", "dir": "libwebp-1.3.2",
"license": "COPYING", "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": [ "build": [
cmd_rmdir(r"output\release-static"), # clean *cmds_cmake(
cmd_nmake( "webp webpdemux webpmux",
"Makefile.vc", "-DBUILD_SHARED_LIBS:BOOL=OFF",
"all", "-DWEBP_LINK_STATIC:BOOL=OFF",
[
"CFG=release-static",
"RTLIBCFG=dynamic",
"OBJDIR=output",
"ARCH={architecture}",
"LIBWEBP_BASENAME=webp",
],
), ),
cmd_mkdir(r"{inc_dir}\webp"), cmd_mkdir(r"{inc_dir}\webp"),
cmd_copy(r"src\webp\*.h", 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": { "libtiff": {
"url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz",
@ -203,8 +202,8 @@ DEPS = {
"#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501
}, },
r"libtiff\tif_webp.c": { r"libtiff\tif_webp.c": {
# link against webp.lib # link against libwebp.lib
"#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501
}, },
r"test\CMakeLists.txt": { r"test\CMakeLists.txt": {
"add_executable(test_write_read_tags ../placeholder.h)": "", "add_executable(test_write_read_tags ../placeholder.h)": "",
@ -217,6 +216,7 @@ DEPS = {
*cmds_cmake( *cmds_cmake(
"tiff", "tiff",
"-DBUILD_SHARED_LIBS:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF",
"-DWebP_LIBRARY=libwebp",
'-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"',
) )
], ],
@ -651,7 +651,7 @@ if __name__ == "__main__":
( (
"ARM64" "ARM64"
if platform.machine() == "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)", help="build architecture (default: same as host Python)",