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