Merge branch 'python-pillow:main' into main

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +0,0 @@
if: tag IS present OR type = api
env:
global:
- CIBW_ARCHS=aarch64
- CIBW_SKIP=pp38-*
language: python
# Default Python version is usually 3.6
python: "3.12"
dist: jammy
services: docker
jobs:
include:
- name: "manylinux2014 aarch64"
os: linux
arch: arm64
env:
- CIBW_BUILD="*manylinux*"
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
- name: "manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- CIBW_BUILD="*manylinux*"
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
- name: "musllinux aarch64"
os: linux
arch: arm64
env:
- CIBW_BUILD="*musllinux*"
install:
- python3 -m pip install -r .ci/requirements-cibw.txt
script:
- python3 -m cibuildwheel --output-dir wheelhouse
- ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
# Upload wheels to GitHub Releases
deploy:
provider: releases
api_key: $GITHUB_RELEASE_TOKEN
file_glob: true
file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
on:
repo: python-pillow/Pillow
tags: true
skip_cleanup: true

View File

@ -2,6 +2,27 @@
Changelog (Pillow)
==================
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)
-------------------

View File

@ -48,9 +48,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
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

View File

@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

BIN
Tests/images/hopper.pfm Normal file

Binary file not shown.

BIN
Tests/images/hopper_be.pfm Normal file

Binary file not shown.

View File

@ -689,3 +689,12 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat
)
with Image.open(test_file) as reloaded:
assert reloaded.mode == mode
def test_apng_repeated_seeks_give_correct_info() -> None:
with Image.open("Tests/images/apng/different_durations.png") as im:
for i in range(3):
im.seek(0)
assert im.info["duration"] == 4000
im.seek(1)
assert im.info["duration"] == 1000

View File

@ -270,7 +270,7 @@ def test_render_scale1():
image1_scale1_compare.load()
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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,6 +55,43 @@ The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant
for internal use, so there is no replacement. They can each be replaced
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

View File

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

View File

@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
: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

View File

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

View File

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

View File

@ -8,9 +8,34 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management
support using the LittleCMS2 color management engine, based on Kevin
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]

View File

@ -0,0 +1,40 @@
.. py:module:: PIL.ImageTransform
.. py:currentmodule:: PIL.ImageTransform
:py:mod:`~PIL.ImageTransform` Module
====================================
The :py:mod:`~PIL.ImageTransform` module contains implementations of
:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin
:py:class:`.Image.Transform` methods.
.. autoclass:: Transform
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: AffineTransform
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: PerspectiveTransform
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: ExtentTransform
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: QuadTransform
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: MeshTransform
:members:
:undoc-members:
:show-inheritance:

View File

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

View File

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

View File

@ -0,0 +1,81 @@
10.3.0
------
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
ImageCms constants and versions() function
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A number of constants and a function in :py:mod:`.ImageCms` have been deprecated.
This includes a table of flags based on LittleCMS version 1 which has been replaced
with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags.
============================================ ====================================================
Deprecated Use instead
============================================ ====================================================
``ImageCms.DESCRIPTION`` No replacement
``ImageCms.VERSION`` ``PIL.__version__``
``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION`
``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT`
``ImageCms.FLAGS["MATRIXONLY"]`` No replacement
``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP`
``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION`
``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS`
``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE`
``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE`
``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM`
``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC`
``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC`
``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK`
``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION`
``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING`
``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES`
``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF`
``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()`
``ImageCms.versions()`` :py:func:`PIL.features.version_module` with
``feature="littlecms2"``, :py:data:`sys.version` or
:py:data:`sys.version_info`, and ``PIL.__version__``
============================================ ====================================================
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
Added PerspectiveTransform
^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning
that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding
subclass of :py:class:`~PIL.ImageTransform.Transform`.
Security
========
TODO
^^^^
TODO
Other Changes
=============
Portable FloatMap (PFM) images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support has been added for reading and writing grayscale (Pf format)
Portable FloatMap (PFM) files containing ``F`` data.

View File

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

View File

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

View File

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

View File

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

View File

@ -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$',

View File

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

View File

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

View File

@ -242,7 +242,7 @@ MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "
_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B")
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.

View File

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

View File

@ -514,7 +514,7 @@ class Parser:
# --------------------------------------------------------------------
def _save(im, fp, tile, bufsize=0):
def _save(im, fp, tile, bufsize=0) -> None:
"""Helper to save image based on tile list
: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

View File

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

View File

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

View File

@ -62,12 +62,14 @@ class LutBuilder:
"""
def __init__(self, patterns=None, op_name=None):
def __init__(
self, patterns: list[str] | None = None, op_name: str | None = None
) -> None:
if patterns is not None:
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

View File

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

View File

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

View File

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

View File

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

View 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"

View 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)

View File

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

View File

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

View File

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

View File

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

View 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"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) {
image = PyObject_CallFunction(fill, "ii", width, height);
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);
}

View File

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

View File

@ -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) */

View File

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

View File

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

View File

@ -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)",