Merge branch 'main' into imagecms_core

This commit is contained in:
Andrew Murray 2024-09-08 23:21:25 +10:00 committed by GitHub
commit 72bc56b319
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
246 changed files with 6138 additions and 2995 deletions

View File

@ -51,7 +51,7 @@ build_script:
test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
@ -37,19 +38,22 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports free-threading
if [[ "$PYTHON_GIL" == "0" ]]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6
# TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
fi
# Pyroma uses non-isolated build and fails with old setuptools
if [[
$GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.9
]]; then
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=67.8"
fi

View File

@ -1 +1 @@
cibuildwheel==2.19.2
cibuildwheel==2.20.0

View File

@ -1 +1,12 @@
mypy==1.10.1
mypy==1.11.2
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pytest
sphinx
types-atheris
types-defusedxml
types-olefile
types-setuptools

View File

@ -3,7 +3,7 @@
BasedOnStyle: Google
AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: AlwaysBreak
AlignAfterOpenBracket: BlockIndent
BinPackArguments: false
BinPackParameters: false
BreakBeforeBraces: Attach

View File

@ -24,6 +24,8 @@ concurrency:
jobs:
Fuzzing:
# Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+
if: false
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers

View File

@ -2,6 +2,9 @@
set -e
if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \
freetype \
ghostscript \
@ -20,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov

View File

@ -74,6 +74,7 @@ jobs:
perl
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
@ -87,7 +87,7 @@ jobs:
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -42,7 +42,6 @@ jobs:
]
python-version: [
"pypy3.10",
"pypy3.9",
"3.13",
"3.12",
"3.11",
@ -50,26 +49,24 @@ jobs:
"3.9",
]
include:
- python-version: "3.11"
PYTHONOPTIMIZE: 1
REVERSE: "--reverse"
- python-version: "3.10"
PYTHONOPTIMIZE: 2
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
# M1 only available for 3.10+
- os: "macos-13"
python-version: "3.9"
- { os: "macos-13", python-version: "3.9" }
exclude:
- os: "macos-14"
python-version: "3.9"
- { os: "macos-14", python-version: "3.9" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
if: "${{ !matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@ -78,6 +75,18 @@ jobs:
".ci/*.sh"
"pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
nogil: ${{ matrix.disable-gil }}
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information
run: python3 .github/workflows/system-info.py

View File

@ -12,9 +12,15 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
# TODO Update condition when NumPy supports free-threading
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
fi
if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip

View File

@ -1,6 +1,14 @@
name: Wheels
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
- cron: "42 1 * * 0,3"
push:
paths:
- ".ci/requirements-cibw.txt"
@ -33,25 +41,21 @@ env:
jobs:
build-1-QEMU-emulated-wheels:
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- pp39
- pp310
- cp39
- cp310
- cp311
- cp312
- cp313
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp39, spec: musllinux }
- { python-version: pp310, spec: musllinux }
steps:
@ -91,6 +95,7 @@ jobs:
path: ./wheelhouse/*.whl
build-2-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
@ -132,6 +137,7 @@ jobs:
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_FREE_THREADED_SUPPORT: True
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
@ -143,6 +149,7 @@ jobs:
path: ./wheelhouse/*.whl
windows:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest
strategy:
@ -204,6 +211,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_FREE_THREADED_SUPPORT: True
CIBW_PRERELEASE_PYTHONS: True
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
@ -228,6 +236,7 @@ jobs:
path: winbuild\build\bin\fribidi*
sdist:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -246,8 +255,25 @@ jobs:
name: dist-sdist
path: dist/*.tar.gz
scientific-python-nightly-wheels-publish:
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
needs: [build-2-native-wheels, windows]
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
runs-on: ubuntu-latest
name: Upload release to PyPI

View File

@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
rev: v0.6.3
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
rev: 24.8.0
hooks:
- id: black
@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.6
rev: 0.29.2
hooks:
- id: check-github-workflows
- id: check-readthedocs
@ -62,12 +62,12 @@ repos:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.1.3
rev: 2.2.1
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.18
rev: v0.19
hooks:
- id: validate-pyproject

View File

@ -2,6 +2,93 @@
Changelog (Pillow)
==================
11.0.0 (unreleased)
-------------------
- Removed unused TiffImagePlugin IFD_LEGACY_API #8355
[radarhere]
- Handle duplicate EXIF header #8350
[zakajd, radarhere]
- Return early from BoxBlur if either width or height is zero #8347
[radarhere]
- Check text is either string or bytes #8308
[radarhere]
- Added writing XMP bytes to JPEG #8286
[radarhere]
- Support JPEG2000 RGBA palettes #8256
[radarhere]
- Expand C image to match GIF frame image size #8237
[radarhere]
- Allow saving I;16 images as PPM #8231
[radarhere]
- When IFD is missing, connect get_ifd() dictionary to Exif #8230
[radarhere]
- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180
[radarhere]
- Treat unknown JPEG2000 colorspace as unspecified #8343
[radarhere]
- Updated error message when saving WebP with invalid width or height #8322
[radarhere, hugovk]
- Remove warning if NumPy failed to raise an error during conversion #8326
[radarhere]
- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304
[radarhere]
- Remove WebP support without anim, mux/demux, and with buggy alpha #8213
[homm, radarhere]
- Add missing TIFF CMYK;16B reader #8298
[homm]
- Remove all WITH_* flags from _imaging.c and other flags #8211
[homm]
- Improve ImageDraw2 shape methods #8265
[radarhere]
- Lock around usages of imaging memory arenas #8238
[lysnikolaou]
- Deprecate JpegImageFile huffman_ac and huffman_dc #8274
[radarhere]
- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242
[radarhere]
- Changed ContainerIO to subclass IO #8240
[radarhere]
- Move away from APIs that use borrowed references under the free-threaded build #8216
[hugovk, lysnikolaou]
- Allow size argument to resize() to be a NumPy array #8201
[radarhere]
- Drop support for Python 3.8 #8183
[hugovk, radarhere]
- Add support for Python 3.13 #8181
[hugovk, radarhere]
- Fix incompatibility with NumPy 1.20 #8187
[neutrinoceros, radarhere]
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
[hugovk, radarhere]
10.4.0 (2024-07-01)
-------------------

View File

@ -60,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b
def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

View File

@ -16,8 +16,9 @@
import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports():
with instrument_imports():
import sys
import fuzzers

View File

@ -14,8 +14,9 @@
import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports():
with instrument_imports():
import sys
import fuzzers

View File

@ -1,5 +1,5 @@
{
<py3_8_encode_current_locale>
<py3_10_encode_current_locale>
Memcheck:Cond
...
fun:encode_current_locale

View File

@ -71,6 +71,11 @@ def test_color_modes() -> None:
box_blur(sample.convert("YCbCr"))
@pytest.mark.parametrize("size", ((0, 1), (1, 0)))
def test_zero_dimension(size: tuple[int, int]) -> None:
assert box_blur(Image.new("L", size)).size == size
def test_radius_0() -> None:
assert_blur(
sample,

View File

@ -105,91 +105,65 @@ class TestColorLut3DCoreAPI:
with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
def test_correct_args(self) -> None:
@pytest.mark.parametrize(
"lut_mode, table_channels, table_size",
[
("RGB", 3, 3),
("CMYK", 4, 3),
("RGB", 3, (2, 3, 3)),
("RGB", 3, (65, 3, 3)),
("RGB", 3, (3, 65, 3)),
("RGB", 3, (2, 3, 65)),
],
)
def test_correct_args(
self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int]
) -> None:
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im.im.color_lut_3d(
"CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im.im.color_lut_3d(
"RGB",
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 3, 3)),
*self.generate_identity_table(table_channels, table_size),
)
@pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("L", "RGB", 3, 3),
("RGB", "L", 3, 3),
("L", "L", 3, 3),
("RGB", "RGBA", 3, 3),
("RGB", "RGB", 4, 3),
],
)
def test_wrong_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new(image_mode, (10, 10), 0)
im.im.color_lut_3d(
"RGB",
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (65, 3, 3)),
*self.generate_identity_table(table_channels, table_size),
)
@pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("RGBA", "RGBA", 3, 3),
("RGBA", "RGBA", 4, 3),
("RGB", "HSV", 3, 3),
("RGB", "RGBA", 4, 3),
],
)
def test_correct_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
im = Image.new(image_mode, (10, 10), 0)
im.im.color_lut_3d(
"RGB",
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 65, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 3, 65)),
)
def test_wrong_mode(self) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_correct_mode(self) -> None:
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
*self.generate_identity_table(table_channels, table_size),
)
def test_identities(self) -> None:

View File

@ -10,11 +10,6 @@ from PIL import features
from .helper import skip_unless_feature
try:
from PIL import _webp
except ImportError:
pass
def test_check() -> None:
# Check the correctness of the convenience function
@ -23,6 +18,10 @@ def test_check() -> None:
for codec in features.codecs:
assert features.check_codec(codec) == features.check(codec)
for feature in features.features:
if "webp" in feature:
with pytest.warns(DeprecationWarning):
assert features.check_feature(feature) == features.check(feature)
else:
assert features.check_feature(feature) == features.check(feature)
@ -48,23 +47,26 @@ def test_version() -> None:
for codec in features.codecs:
test(codec, features.version_codec)
for feature in features.features:
if "webp" in feature:
with pytest.warns(DeprecationWarning):
test(feature, features.version_feature)
else:
test(feature, features.version_feature)
@skip_unless_feature("webp")
def test_webp_transparency() -> None:
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha()
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY
with pytest.warns(DeprecationWarning):
assert features.check("transp_webp") == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_mux() -> None:
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX
with pytest.warns(DeprecationWarning):
assert features.check("webp_mux") == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_anim() -> None:
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM
with pytest.warns(DeprecationWarning):
assert features.check("webp_anim") == features.check_module("webp")
@skip_unless_feature("libjpeg_turbo")

View File

@ -1,7 +1,5 @@
from __future__ import annotations
from typing import Literal
import pytest
from PIL import ContainerIO, Image
@ -23,6 +21,13 @@ def test_isatty() -> None:
assert container.isatty() is False
def test_seekable() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
assert container.seekable() is True
@pytest.mark.parametrize(
"mode, expected_position",
(
@ -31,7 +36,7 @@ def test_isatty() -> None:
(2, 100),
),
)
def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
def test_seek_mode(mode: int, expected_position: int) -> None:
# Arrange
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@ -44,6 +49,14 @@ def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
assert container.tell() == expected_position
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readable(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.readable() is True
@pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n0(bytesmode: bool) -> None:
# Arrange
@ -51,7 +64,7 @@ def test_read_n0(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
assert container.seek(81) == 81
data = container.read()
# Assert
@ -67,7 +80,7 @@ def test_read_n(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
assert container.seek(81) == 81
data = container.read(3)
# Assert
@ -83,7 +96,7 @@ def test_read_eof(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(100)
assert container.seek(100) == 100
data = container.read()
# Assert
@ -94,21 +107,65 @@ def test_read_eof(bytesmode: bool) -> None:
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readline()
# Assert
if bytesmode:
data = data.decode()
assert data == "This is line 1\n"
data = container.readline(4)
if bytesmode:
data = data.decode()
assert data == "This"
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode: bool) -> None:
expected = [
"This is line 1\n",
"This is line 2\n",
"This is line 3\n",
"This is line 4\n",
"This is line 5\n",
"This is line 6\n",
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
data = container.readlines()
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
assert container.seek(0) == 0
data = container.readlines(2)
if bytesmode:
data = [line.decode() for line in data]
assert data == expected[:2]
@pytest.mark.parametrize("bytesmode", (True, False))
def test_write(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.writable() is False
with pytest.raises(NotImplementedError):
container.write(b"" if bytesmode else "")
with pytest.raises(NotImplementedError):
container.writelines([])
with pytest.raises(NotImplementedError):
container.truncate()
@pytest.mark.parametrize("bytesmode", (True, False))
def test_iter(bytesmode: bool) -> None:
# Arrange
expected = [
"This is line 1\n",
@ -124,9 +181,21 @@ def test_readlines(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readlines()
data = []
for line in container:
data.append(line)
# Assert
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
@pytest.mark.parametrize("bytesmode", (True, False))
def test_file(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert isinstance(container.fileno(), int)
container.flush()
container.close()

View File

@ -152,7 +152,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None:
@pytest.mark.parametrize(
("image_path", "expected_path"),
"image_path, expected_path",
(
# hexeditted to be typeless
(TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM),
@ -248,7 +248,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None:
@pytest.mark.parametrize(
("mode", "size", "test_file"),
"mode, size, test_file",
[
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
@ -373,7 +373,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
@pytest.mark.parametrize(
("mode", "test_file"),
"mode, test_file",
[
("L", "Tests/images/linear_gradient.png"),
("LA", "Tests/images/uncompressed_la.png"),

View File

@ -80,9 +80,7 @@ simple_eps_file_with_long_binary_data = (
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize(
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
)
@pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252))))
@pytest.mark.parametrize("scale", (1, 2))
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size)

View File

@ -353,7 +353,7 @@ def test_palette_434(tmp_path: Path) -> None:
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
im.copy().save(out, **kwargs)
im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)
return reloaded
@ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
# Test opaque WebP background
if features.check("webp") and features.check("webp_anim"):
if features.check("webp"):
with Image.open("Tests/images/hopper.webp") as im:
assert im.info["background"] == (255, 255, 255, 255)
im.save(out)
@ -1378,8 +1378,26 @@ def test_lzw_bits() -> None:
im.load()
def test_extents() -> None:
with Image.open("Tests/images/test_extents.gif") as im:
@pytest.mark.parametrize(
"test_file, loading_strategy",
(
("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),
(
"test_extents.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
),
(
"test_extents_transparency.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
),
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
# Check that n_frames does not change the size
@ -1389,6 +1407,11 @@ def test_extents() -> None:
im.seek(1)
assert im.size == (150, 150)
im.load()
assert im.im.size == (150, 150)
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_missing_background() -> None:
# The Global Color Table Flag isn't set, so there is no background color index,

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest
from PIL import IcoImagePlugin, Image, ImageDraw
from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
@ -241,3 +241,29 @@ def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(outfile) as im:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_truncated_mask() -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()
ImageFile.LOAD_TRUNCATED_IMAGES = True
data = data[:-3]
try:
with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected:
assert im.mode == "1"
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
data = output.getvalue()[:-1]
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False

View File

@ -57,6 +57,7 @@ def test_getiptcinfo_fotostation() -> None:
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert iptc is not None
for tag in iptc.keys():
if tag[0] == 240:
return
@ -76,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None:
assert len(iptc) == 3
def test_getiptcinfo_tiff() -> None:
# Arrange
with Image.open("Tests/images/hopper.Lab.tif") as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"}
def test_getiptcinfo_tiff_none() -> None:
# Arrange
with Image.open("Tests/images/hopper.tif") as im:

View File

@ -154,7 +154,7 @@ class TestFileJpeg:
assert k > 0.9
def test_rgb(self) -> None:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
return tuple(v[0] for v in im.layer)
im = hopper()
@ -829,7 +829,7 @@ class TestFileJpeg:
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
# Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated."
# https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html
# https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self) -> None:
@ -991,12 +991,29 @@ class TestFileJpeg:
else:
assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"
im.info["xmp"] = b"1" * 65504
im.save(f)
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504
with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1)
def test_eof(self) -> None:
# Even though this decoder never says that it is finished
# the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(
self, buffer: bytes | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0
Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
@ -1019,13 +1036,16 @@ class TestFileJpeg:
# SOI, EOI
for marker in b"\xff\xd8", b"\xff\xd9":
assert marker in data[1] and marker in data[2]
assert marker in data[1]
assert marker in data[2]
# DHT, DQT
for marker in b"\xff\xc4", b"\xff\xdb":
assert marker in data[1] and marker not in data[2]
assert marker in data[1]
assert marker not in data[2]
# SOF0, SOS, APP0 (JFIF header)
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
assert marker not in data[1] and marker in data[2]
assert marker not in data[1]
assert marker in data[2]
with Image.open(BytesIO(data[0])) as interchange_im:
with Image.open(BytesIO(data[1] + data[2])) as combined_im:
@ -1045,6 +1065,13 @@ class TestFileJpeg:
assert im._repr_jpeg_() is None
def test_deprecation(self) -> None:
with Image.open(TEST_FILE) as im:
with pytest.warns(DeprecationWarning):
assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning):
assert im.huffman_dc == {}
@pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg")

View File

@ -182,6 +182,15 @@ def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_unknown_colorspace() -> None:
with Image.open(f"{EXTRA_DIR}/file8.jp2") as im:
im.load()
assert im.mode == "L"
def test_header_errors() -> None:
for path in (
"Tests/images/invalid_header_length.jp2",
@ -233,7 +242,7 @@ def test_layers() -> None:
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
(None, {"no_jp2": False}, 4, b"jP"),
),
)
def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None:
@ -391,6 +400,13 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0
def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:

View File

@ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f:
s.write(f.read())
s.seek(0)
r = io.BufferedReader(s)
data = f.read()
class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result
def readable(self) -> bool:
return True
r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im:
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
@ -229,9 +240,10 @@ class TestFileLibTiff(LibTiffTestCase):
new_ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, info in core_items.items():
assert info.type is not None
if info.length == 1:
new_ifd[tag] = values[info.type]
if info.length == 0:
elif not info.length:
new_ifd[tag] = tuple(values[info.type] for _ in range(3))
else:
new_ifd[tag] = tuple(values[info.type] for _ in range(info.length))
@ -1048,7 +1060,11 @@ class TestFileLibTiff(LibTiffTestCase):
],
)
def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile
self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
@ -1135,7 +1151,7 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import warnings
from io import BytesIO
from typing import Any, cast
from typing import Any
import pytest
from PIL import Image, MpoImagePlugin
from PIL import Image, ImageFile, MpoImagePlugin
from .helper import (
assert_image_equal,
@ -20,11 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg")
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "MPO", **options)
out.seek(0)
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
return Image.open(out)
@pytest.mark.parametrize("test_file", test_files)
@ -85,7 +85,9 @@ def test_exif(test_file: str) -> None:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
for im in (im_original, im_reloaded):
assert isinstance(im, MpoImagePlugin.MpoImageFile)
info = im._getexif()
assert info is not None
assert info[272] == "Nintendo 3DS"
assert info[296] == 2
assert info[34665] == 188
@ -226,6 +228,12 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)
def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
@ -275,6 +283,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)

View File

@ -118,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params)
im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
contents = fp.read()
@ -229,6 +229,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
assert pdf.pages_ref is not None
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info

View File

@ -41,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC
def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data)
PngImagePlugin.putchunk(test_file, cid, *data)
return test_file.getvalue()
@ -424,8 +424,10 @@ class TestFilePng:
im = roundtrip(im, pnginfo=info)
assert im.info == {"spam": "Eggs", "eggs": "Spam"}
assert im.text == {"spam": "Eggs", "eggs": "Spam"}
assert isinstance(im.text["spam"], PngImagePlugin.iTXt)
assert im.text["spam"].lang == "en"
assert im.text["spam"].tkey == "Spam"
assert isinstance(im.text["eggs"], PngImagePlugin.iTXt)
assert im.text["eggs"].lang == "en"
assert im.text["eggs"].tkey == "Eggs"
@ -776,7 +778,7 @@ class TestFilePng:
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout # type: ignore[assignment]
sys.stdout = mystdout
with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG")

View File

@ -95,7 +95,9 @@ def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = str(tmp_path / "temp.pgm")
im.save(filename, "PPM")
assert_image_equal_tofile(im, filename)
im.convert("I;16").save(filename, "PPM")
assert_image_equal_tofile(im, filename)
@ -373,7 +375,7 @@ def test_save_stdout(buffer: bool) -> None:
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout # type: ignore[assignment]
sys.stdout = mystdout
with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM")

View File

@ -78,6 +78,7 @@ class TestFileTiff:
def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff")
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close()
with pytest.raises(ValueError):
@ -424,13 +425,13 @@ class TestFileTiff:
def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd"
ret = ifd.load_float(data, False)
ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False)
ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194)
def test_ifd_tag_type(self) -> None:
@ -599,7 +600,7 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs)
hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
@ -624,7 +625,9 @@ class TestFileTiff:
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif")
im = hopper()
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
@ -632,6 +635,7 @@ class TestFileTiff:
im.save(outfile)
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None:
@ -680,6 +684,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)
def test_invalid_tiled_dimensions(self) -> None:
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
data = fp.read()
b = BytesIO(data[:144] + b"\x02" + data[145:])
with pytest.raises(ValueError):
Image.open(b)
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")

View File

@ -48,8 +48,6 @@ class TestFileWebp:
self.rgb_mode = "RGB"
def test_version(self) -> None:
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
version = features.version_module("webp")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@ -117,7 +115,6 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@skip_unless_feature("webp_anim")
def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (1, 1))
@ -132,7 +129,6 @@ class TestFileWebp:
def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM:
self._roundtrip(
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
)
@ -161,27 +157,32 @@ class TestFileWebp:
im.save(temp_file, method=0)
assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("L", (16384, 16384))
with pytest.raises(ValueError) as e:
im.save(temp_file)
assert (
str(e.value)
== "encoding error 5: Image size exceeds WebP limit of 16383 pixels"
)
def test_WebPEncode_with_invalid_args(self) -> None:
"""
Calling encoder functions with no arguments should result in an error.
"""
if _webp.HAVE_WEBPANIM:
with pytest.raises(TypeError):
_webp.WebPAnimEncoder()
with pytest.raises(TypeError):
_webp.WebPEncode()
def test_WebPDecode_with_invalid_args(self) -> None:
def test_WebPAnimDecoder_with_invalid_args(self) -> None:
"""
Calling decoder functions with no arguments should result in an error.
"""
if _webp.HAVE_WEBPANIM:
with pytest.raises(TypeError):
_webp.WebPAnimDecoder()
with pytest.raises(TypeError):
_webp.WebPDecode()
def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp"
@ -200,7 +201,6 @@ class TestFileWebp:
"background",
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
)
@skip_unless_feature("webp_anim")
def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
@ -209,7 +209,6 @@ class TestFileWebp:
with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background)
@skip_unless_feature("webp_anim")
def test_background_from_gif(self, tmp_path: Path) -> None:
# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
@ -234,7 +233,6 @@ class TestFileWebp:
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
assert difference < 5
@skip_unless_feature("webp_anim")
def test_duration(self, tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000
@ -250,6 +248,7 @@ class TestFileWebp:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "RGBA"
im.save(temp_file)

View File

@ -13,12 +13,7 @@ from .helper import (
hopper,
)
_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed")
def setup_module() -> None:
if _webp.WebPDecoderBuggyAlpha():
pytest.skip("Buggy early version of WebP installed, not testing transparency")
pytest.importorskip("PIL._webp", reason="WebP support not installed")
def test_read_rgba() -> None:
@ -81,9 +76,6 @@ def test_write_rgba(tmp_path: Path) -> None:
pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
pil_image.save(temp_file)
if _webp.WebPDecoderBuggyAlpha():
return
with Image.open(temp_file) as image:
image.load()
@ -93,11 +85,6 @@ def test_write_rgba(tmp_path: Path) -> None:
image.load()
image.getdata()
# Early versions of WebP are known to produce higher deviations:
# deal with it
if _webp.WebPDecoderVersion() <= 0x201:
assert_image_similar(image, pil_image, 3.0)
else:
assert_image_similar(image, pil_image, 1.0)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Generator
from pathlib import Path
import pytest
@ -14,10 +15,7 @@ from .helper import (
skip_unless_feature,
)
pytestmark = [
skip_unless_feature("webp"),
skip_unless_feature("webp_anim"),
]
pytestmark = skip_unless_feature("webp")
def test_n_frames() -> None:
@ -96,7 +94,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
check(temp_file1)
# Tests appending using a generator
def im_generator(ims):
def im_generator(
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp")

View File

@ -8,14 +8,11 @@ from PIL import Image
from .helper import assert_image_equal, hopper
_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed")
pytest.importorskip("PIL._webp", reason="WebP support not installed")
RGB_MODE = "RGB"
def test_write_lossless_rgb(tmp_path: Path) -> None:
if _webp.WebPDecoderVersion() < 0x0200:
pytest.skip("lossless not included")
temp_file = str(tmp_path / "temp.webp")
hopper(RGB_MODE).save(temp_file, lossless=True)

View File

@ -10,10 +10,7 @@ from PIL import Image
from .helper import mark_if_feature_version, skip_unless_feature
pytestmark = [
skip_unless_feature("webp"),
skip_unless_feature("webp_mux"),
]
pytestmark = skip_unless_feature("webp")
ElementTree: ModuleType | None
try:
@ -119,7 +116,15 @@ def test_read_no_exif() -> None:
def test_getxmp() -> None:
with Image.open("Tests/images/flower.webp") as im:
assert "xmp" not in im.info
assert im.getxmp() == {}
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None:
@ -136,7 +141,6 @@ def test_getxmp() -> None:
)
@skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path: Path) -> None:
iccp_data = b"<iccp_data>"
exif_data = b"<exif_data>"

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import os
from pathlib import Path
from typing import AnyStr
import pytest
@ -92,7 +93,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None:
def _test_high_characters(
request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes
request: pytest.FixtureRequest, tmp_path: Path, message: AnyStr
) -> None:
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)

View File

@ -42,6 +42,12 @@ try:
except ImportError:
ElementTree = None
PrettyPrinter: type | None
try:
from IPython.lib.pretty import PrettyPrinter
except ImportError:
PrettyPrinter = None
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -91,16 +97,15 @@ class TestImage:
# with pytest.raises(MemoryError):
# Image.new("L", (1000000, 1000000))
@pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
def test_repr_pretty(self) -> None:
class Pretty:
def text(self, text: str) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100))
p = Pretty()
im._repr_pretty_(p, None)
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>"
output = io.StringIO()
assert PrettyPrinter is not None
p = PrettyPrinter(output)
im._repr_pretty_(p, False)
assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"
def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png"
@ -372,8 +377,9 @@ class TestImage:
img = Image.alpha_composite(dst, src)
# Assert
img_colors = sorted(img.getcolors())
assert img_colors == expected_colors
img_colors = img.getcolors()
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
@ -670,7 +676,9 @@ class TestImage:
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6
palette = im_remapped.getpalette()
assert palette is not None
assert len(palette) == 6
# Test unused transparency
im.info["transparency"] = 2
@ -697,11 +705,12 @@ class TestImage:
assert new_image.size == image.size
assert new_image.info == base_image.info
if palette_result is not None:
assert new_image.palette is not None
assert new_image.palette.tobytes() == palette_result.tobytes()
else:
assert new_image.palette is None
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
_make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
@ -766,6 +775,22 @@ class TestImage:
exif.load(b"Exif\x00\x00")
assert not dict(exif)
def test_duplicate_exif_header(self) -> None:
with Image.open("Tests/images/exif.png") as im:
im.load()
im.info["exif"] = b"Exif\x00\x00" + im.info["exif"]
exif = im.getexif()
assert exif[274] == 1
def test_empty_get_ifd(self) -> None:
exif = Image.Exif()
ifd = exif.get_ifd(0x8769)
assert ifd == {}
ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"}
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@ -814,7 +839,6 @@ class TestImage:
assert reloaded_exif[305] == "Pillow test"
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_exif_webp(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.webp") as im:
exif = im.getexif()
@ -936,7 +960,15 @@ class TestImage:
def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {}
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1))
@ -987,12 +1019,14 @@ class TestImage:
# P mode with RGBA palette
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "RGBA"
assert im.has_transparency_data
def test_apply_transparency(self) -> None:
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1))
assert im.palette is not None
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
# Test that no transformation is applied without transparency
@ -1010,13 +1044,16 @@ class TestImage:
im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
im.info["transparency"] = 0
im.apply_transparency()
assert im.palette is not None
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1}
# Test that transparency bytes are applied
with Image.open("Tests/images/pil123p.png") as im:
assert isinstance(im.info["transparency"], bytes)
assert im.palette is not None
assert im.palette.colors[(27, 35, 6)] == 24
im.apply_transparency()
assert im.palette is not None
assert im.palette.colors[(27, 35, 6, 214)] == 24
def test_constants(self) -> None:

View File

@ -27,7 +27,9 @@ class TestImagePutPixel:
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@ -37,7 +39,9 @@ class TestImagePutPixel:
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@ -50,9 +54,9 @@ class TestImagePutPixel:
assert pix1 is not None
assert pix2 is not None
with pytest.raises(TypeError):
pix1[0, "0"]
pix1[0, "0"] # type: ignore[index]
with pytest.raises(TypeError):
pix1["0", 0]
pix1["0", 0] # type: ignore[index]
for y in range(im1.size[1]):
for x in range(im1.size[0]):
@ -71,7 +75,9 @@ class TestImagePutPixel:
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@ -81,7 +87,9 @@ class TestImagePutPixel:
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@ -219,10 +227,10 @@ class TestImagePutPixelError:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"):
im.putpixel((0, 0), v)
im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize(
("mode", "band_numbers", "match"),
"mode, band_numbers, match",
(
("L", (0, 2), "color must be int or single-element tuple"),
("LA", (0, 3), "color must be int, or tuple of one or two elements"),
@ -253,7 +261,7 @@ class TestImagePutPixelError:
with pytest.raises(
TypeError, match="color must be int or single-element tuple"
):
im.putpixel((0, 0), v)
im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode: str) -> None:

View File

@ -24,7 +24,7 @@ def test_toarray() -> None:
def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype
assert ai.dtype.type is dtype
# assert test("1") == ((100, 128), '|b1', 1600))
assert test("L") == ((100, 128), "|u1", 12800)
@ -47,7 +47,7 @@ def test_toarray() -> None:
with pytest.raises(OSError):
numpy.array(im_truncated)
else:
with pytest.warns(UserWarning):
with pytest.warns(DeprecationWarning):
numpy.array(im_truncated)
@ -113,4 +113,5 @@ def test_fromarray_palette() -> None:
out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match
assert out.palette is not None
assert len(out.palette.colors) == len(out.im.getpalette()) / 3

View File

@ -218,6 +218,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
def test_l_macro_rounding(convert_mode: str) -> None:
for mode in ("P", "PA"):
im = Image.new(mode, (1, 1))
assert im.palette is not None
im.palette.getcolor((0, 1, 2))
converted_im = im.convert(convert_mode)
@ -225,7 +226,7 @@ def test_l_macro_rounding(convert_mode: str) -> None:
assert px is not None
converted_color = px[0, 0]
if convert_mode == "LA":
assert converted_color is not None
assert isinstance(converted_color, tuple)
converted_color = converted_color[0]
assert converted_color == 1

View File

@ -54,17 +54,21 @@ def test_pack() -> None:
assert A is None
A = im.getcolors(maxcolors=3)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=4)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=8)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=16)
assert A is not None
A.sort()
assert A == expected

View File

@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None:
# see bug-200802-systemerror
def put(value: int) -> tuple[int, int, int, int]:
def put(value: int) -> float | tuple[int, ...] | None:
im = Image.new("RGBA", (1, 1))
im.putdata([value])
return im.getpixel((0, 0))

View File

@ -86,6 +86,7 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
im = Image.new("P", (1, 1))
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]
assert im.palette is not None
assert im.palette.colors == {(1, 2, 3, 4): 0}

View File

@ -31,7 +31,9 @@ def test_libimagequant_quantize() -> None:
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_octree_quantize() -> None:
@ -39,7 +41,9 @@ def test_octree_quantize() -> None:
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_rgba_quantize() -> None:
@ -65,6 +69,7 @@ def test_quantize_no_dither() -> None:
converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert converted.mode == "P"
assert converted.palette is not None
assert converted.palette.palette == palette.palette.palette
@ -77,6 +82,7 @@ def test_quantize_no_dither2() -> None:
palette.putpalette(data)
quantized = im.quantize(dither=Image.Dither.NONE, palette=palette)
assert quantized.palette is not None
assert tuple(quantized.palette.palette) == data
px = quantized.load()
@ -113,6 +119,7 @@ def test_colors() -> None:
im = hopper()
colors = 2
converted = im.quantize(colors)
assert converted.palette is not None
assert len(converted.palette.palette) == colors * len("RGB")
@ -143,6 +150,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method)
converted_px = converted.load()
assert converted_px is not None
assert converted.palette is not None
assert converted_px[0, 0] == converted.palette.colors[color]
@ -158,4 +166,6 @@ def test_small_palette() -> None:
im = im.quantize(palette=p)
# Assert
assert len(im.getcolors()) == 2
quantized_colors = im.getcolors()
assert quantized_colors is not None
assert len(quantized_colors) == 2

View File

@ -237,13 +237,13 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency:
def make_case(
self, mode: str, fill: tuple[int, int, int] | float
) -> tuple[Image.Image, tuple[int, ...]]:
) -> tuple[Image.Image, float | tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill)
px = im.load()
assert px is not None
return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None:
channel, color = case
px = channel.load()
assert px is not None
@ -256,6 +256,7 @@ class TestCoreResampleConsistency:
def test_8u(self) -> None:
im, color = self.make_case("RGB", (0, 64, 255))
r, g, b = im.split()
assert isinstance(color, tuple)
self.run_case((r, color[0]))
self.run_case((g, color[1]))
self.run_case((b, color[2]))
@ -290,7 +291,11 @@ class TestCoreResampleAlphaCorrect:
px = i.load()
assert px is not None
for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])}
used_colors = set()
for x in range(i.size[0]):
value = px[x, y]
assert isinstance(value, tuple)
used_colors.add(value[0])
assert 256 == len(used_colors), (
"All colors should be present in resized image. "
f"Only {len(used_colors)} on line {y}."
@ -332,12 +337,13 @@ class TestCoreResampleAlphaCorrect:
assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel:
value = px[x, y]
assert isinstance(value, tuple)
if value[-1] != 0 and value[:-1] != clean_pixel:
message = (
f"pixel at ({x}, {y}) is different:\n"
f"{px[x, y]}\n{clean_pixel}"
f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}"
)
assert px[x, y][:3] == clean_pixel, message
assert value[:3] == clean_pixel, message
def test_dirty_pixels_rgba(self) -> None:
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))

View File

@ -285,14 +285,14 @@ class TestReducingGapResize:
class TestImageResize:
def test_resize(self) -> None:
def resize(mode: str, size: tuple[int, int]) -> None:
def resize(mode: str, size: tuple[int, int] | list[int]) -> None:
out = hopper(mode).resize(size)
assert out.mode == mode
assert out.size == size
assert out.size == tuple(size)
for mode in "1", "P", "L", "RGB", "I", "F":
resize(mode, (112, 103))
resize(mode, (188, 214))
resize(mode, [188, 214])
# Test unknown resampling filter
with hopper() as im:

View File

@ -192,8 +192,9 @@ class TestImageTransform:
im = op(im, (40, 10))
colors = sorted(im.getcolors())
assert colors == sorted(
colors = im.getcolors()
assert colors is not None
assert sorted(colors) == sorted(
(
(20 * 10, opaque),
(20 * 10, transparent),

View File

@ -391,23 +391,26 @@ def test_overlay() -> None:
def test_logical() -> None:
def table(
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
) -> tuple[int, int, int, int]:
) -> list[float]:
out = []
for x in (a, b):
imx = Image.new("1", (1, 1), x)
for y in (a, b):
imy = Image.new("1", (1, 1), y)
out.append(op(imx, imy).getpixel((0, 0)))
return tuple(out)
value = op(imx, imy).getpixel((0, 0))
assert not isinstance(value, tuple)
assert value is not None
out.append(value)
return out
assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0]

View File

@ -691,7 +691,9 @@ def test_rgb_lab(mode: str) -> None:
im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
value = converted_im.getpixel((0, 0))
assert isinstance(value, tuple)
assert value[:3] == (0, 255, 255)
def test_deprecation() -> None:

View File

@ -1,8 +1,8 @@
from __future__ import annotations
import contextlib
import os.path
from collections.abc import Sequence
from typing import Callable
import pytest
@ -857,6 +857,27 @@ def test_rounded_rectangle_corners(
)
def test_rounded_rectangle_joined_x_different_corners() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
# Act
draw.rounded_rectangle(
(20, 10, 80, 90),
30,
fill="red",
outline="green",
width=5,
corners=(True, False, False, False),
)
# Assert
assert_image_equal_tofile(
im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png"
)
@pytest.mark.parametrize(
"xy, radius, type",
[
@ -1422,25 +1443,44 @@ def test_default_font_size() -> None:
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def check(func: Callable[[], None]) -> None:
if freetype_support:
func()
else:
with pytest.raises(ImportError):
func()
def draw_text() -> None:
draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_text)
def draw_textlength() -> None:
assert draw.textlength(text, font_size=16) == 216
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_textlength)
def draw_textbbox() -> None:
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_textbbox)
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_multiline_text)
def draw_multiline_textbbox() -> None:
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_multiline_textbbox)
@pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox: Coords) -> None:

View File

@ -65,6 +65,36 @@ def test_mode() -> None:
ImageDraw2.Draw("L")
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("white", width=1)
# Act
draw.arc(bbox, pen, start, end)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
@pytest.mark.parametrize("bbox", BBOX)
def test_chord(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("yellow")
brush = ImageDraw2.Brush("red")
# Act
draw.chord(bbox, pen, 0, 180, brush)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_RGB.png", 1)
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(bbox: Coords) -> None:
# Arrange
@ -123,6 +153,22 @@ def test_line_pen_as_brush(points: Coords) -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("blue")
brush = ImageDraw2.Brush("white")
# Act
draw.pieslice(bbox, pen, start, end, brush)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
@pytest.mark.parametrize("points", POINTS)
def test_polygon(points: Coords) -> None:
# Arrange

View File

@ -90,10 +90,10 @@ class TestImageFile:
data = f.read()
with ImageFile.Parser() as p:
p.feed(data)
assert p.image is not None
assert (48, 48) == p.image.size
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p:
with open("Tests/images/hopper.webp", "rb") as f:
@ -103,6 +103,7 @@ class TestImageFile:
assert not p.image
p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size
@skip_unless_feature("zlib")
@ -125,7 +126,7 @@ class TestImageFile:
def test_raise_typeerror(self) -> None:
with pytest.raises(TypeError):
parser = ImageFile.Parser()
parser.feed(1)
parser.feed(1) # type: ignore[arg-type]
def test_negative_stride(self) -> None:
with open("Tests/images/raw_negative_stride.bin", "rb") as f:
@ -209,7 +210,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args)
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
# eof
return -1, 0
@ -237,7 +238,9 @@ class MockImageFile(ImageFile.ImageFile):
self.rawmode = "RGBA"
self._mode = "RGBA"
self._size = (200, 200)
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
self.tile = [
ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)
]
class CodecsTest:
@ -267,7 +270,7 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
im.load()
@ -280,12 +283,12 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
with pytest.raises(ValueError):
im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
with pytest.raises(ValueError):
im.load()
@ -293,19 +296,27 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None
)
]
with pytest.raises(ValueError):
im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)]
im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None
)
]
with pytest.raises(ValueError):
im.load()
def test_decode(self) -> None:
decoder = ImageFile.PyDecoder(None)
decoder = ImageFile.PyDecoder("")
with pytest.raises(NotImplementedError):
decoder.decode(None)
decoder.decode(b"")
class TestPyEncoder(CodecsTest):
@ -316,7 +327,13 @@ class TestPyEncoder(CodecsTest):
fp = BytesIO()
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
im,
fp,
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB"
)
],
)
assert MockPyEncoder.last
@ -329,10 +346,10 @@ class TestPyEncoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])
assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0
@ -349,7 +366,9 @@ class TestPyEncoder(CodecsTest):
MockPyEncoder.last = None
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")],
)
last: MockPyEncoder | None = MockPyEncoder.last
assert last
@ -357,7 +376,9 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")]
im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")],
)
def test_oversize(self) -> None:
@ -370,18 +391,26 @@ class TestPyEncoder(CodecsTest):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")],
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB"
)
],
)
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")],
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB"
)
],
)
def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None)
encoder = ImageFile.PyEncoder("")
with pytest.raises(NotImplementedError):
encoder.encode(0)
@ -394,7 +423,7 @@ class TestPyEncoder(CodecsTest):
encoder.encode_to_pyfd()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)
encoder.encode_to_file(0, 0)
def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError):

View File

@ -717,14 +717,14 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None:
font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
_check_text(font, "Tests/images/variation_adobe.png", 11)
for name in ["Bold", b"Bold"]:
for name in ("Bold", b"Bold"):
font.set_variation_by_name(name)
assert font.getname()[1] == "Bold"
_check_text(font, "Tests/images/variation_adobe_name.png", 16)
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
_check_text(font, "Tests/images/variation_tiny.png", 40)
for name in ["200", b"200"]:
for name in ("200", b"200"):
font.set_variation_by_name(name)
assert font.getname()[1] == "200"
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
@ -1113,6 +1113,9 @@ def test_bytes(font: ImageFont.FreeTypeFont) -> None:
)
assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
with pytest.raises(TypeError):
font.getlength((0, 0)) # type: ignore[arg-type]
@pytest.mark.parametrize(
"test_file",

View File

@ -60,6 +60,8 @@ class TestImageGrab:
def test_grabclipboard(self) -> None:
if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"])
ImageGrab.grabclipboard()
elif sys.platform == "win32":
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write(
@ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200
[Windows.Forms.Clipboard]::SetImage($bmp)"""
)
p.communicate()
ImageGrab.grabclipboard()
else:
if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises(
@ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200
r" ImageGrab.grabclipboard\(\) on Linux",
):
ImageGrab.grabclipboard()
return
ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None:

View File

@ -1,5 +1,9 @@
from __future__ import annotations
from typing import Any
import pytest
from PIL import Image, ImageMath
@ -19,7 +23,7 @@ I = Image.new("I", (1, 1), 4) # noqa: E741
A2 = A.resize((2, 2))
B2 = B.resize((2, 2))
images = {"A": A, "B": B, "F": F, "I": I}
images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}
def test_sanity() -> None:
@ -30,13 +34,13 @@ def test_sanity() -> None:
== "I 3"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images
lambda args: args["float"](args["A"]) + args["B"], **images
)
)
== "F 3.0"
@ -44,42 +48,47 @@ def test_sanity() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["int"](args["float"](args["A"]) + args["B"]), images
lambda args: args["int"](args["float"](args["A"]) + args["B"]), **images
)
)
== "I 3"
)
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.lambda_eval(lambda args: 1, images) == 1
def test_ops() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1"
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], **images))
== "I -1"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], **images))
== "I 2"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], **images))
== "I 0"
)
assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4"
assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, **images)) == "I 4"
assert (
pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images))
pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, **images))
== "I 2147483647"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images
lambda args: args["float"](args["A"]) + args["B"], **images
)
)
== "F 3.0"
@ -87,7 +96,7 @@ def test_ops() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) - args["B"], images
lambda args: args["float"](args["A"]) - args["B"], **images
)
)
== "F -1.0"
@ -95,7 +104,7 @@ def test_ops() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) * args["B"], images
lambda args: args["float"](args["A"]) * args["B"], **images
)
)
== "F 2.0"
@ -103,31 +112,33 @@ def test_ops() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) / args["B"], images
lambda args: args["float"](args["A"]) / args["B"], **images
)
)
== "F 0.5"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images))
pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, **images)
)
== "F 4.0"
)
assert (
pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images)
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, **images)
)
== "F 8589934592.0"
)
def test_logical() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0
assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], **images)) == 0
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], **images))
== "L 2"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], **images))
== "L 1"
)
@ -136,7 +147,7 @@ def test_convert() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "L"), images
lambda args: args["convert"](args["A"] + args["B"], "L"), **images
)
)
== "L 3"
@ -144,7 +155,7 @@ def test_convert() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "1"), images
lambda args: args["convert"](args["A"] + args["B"], "1"), **images
)
)
== "1 0"
@ -152,7 +163,7 @@ def test_convert() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "RGB"), images
lambda args: args["convert"](args["A"] + args["B"], "RGB"), **images
)
)
== "RGB (3, 3, 3)"
@ -163,7 +174,7 @@ def test_compare() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["min"](args["A"], args["B"]), images
lambda args: args["min"](args["A"], args["B"]), **images
)
)
== "I 1"
@ -171,13 +182,13 @@ def test_compare() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["max"](args["A"], args["B"]), images
lambda args: args["max"](args["A"], args["B"]), **images
)
)
== "I 2"
)
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, **images)) == "I 0"
def test_one_image_larger() -> None:

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from typing import Any
import pytest
from PIL import Image, ImageMath
@ -21,16 +23,16 @@ I = Image.new("I", (1, 1), 4) # noqa: E741
A2 = A.resize((2, 2))
B2 = B.resize((2, 2))
images = {"A": A, "B": B, "F": F, "I": I}
images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}
def test_sanity() -> None:
assert ImageMath.unsafe_eval("1") == 1
assert ImageMath.unsafe_eval("1+A", A=2) == 3
assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3"
def test_eval_deprecated() -> None:
@ -38,23 +40,28 @@ def test_eval_deprecated() -> None:
assert ImageMath.eval("1") == 1
def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.unsafe_eval("1", images) == 1
def test_ops() -> None:
assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4"
assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647"
assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A-B", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("A*B", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A/B", **images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("B**2", **images)) == "I 4"
assert pixel(ImageMath.unsafe_eval("B**33", **images)) == "I 2147483647"
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0"
assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0"
assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5"
assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0"
assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0"
assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("float(A)-B", **images)) == "F -1.0"
assert pixel(ImageMath.unsafe_eval("float(A)*B", **images)) == "F 2.0"
assert pixel(ImageMath.unsafe_eval("float(A)/B", **images)) == "F 0.5"
assert pixel(ImageMath.unsafe_eval("float(B)**2", **images)) == "F 4.0"
assert pixel(ImageMath.unsafe_eval("float(B)**33", **images)) == "F 8589934592.0"
@pytest.mark.parametrize(
@ -72,33 +79,33 @@ def test_prevent_exec(expression: str) -> None:
def test_prevent_double_underscores() -> None:
with pytest.raises(ValueError):
ImageMath.unsafe_eval("1", {"__": None})
ImageMath.unsafe_eval("1", __=None)
def test_prevent_builtins() -> None:
with pytest.raises(ValueError):
ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None})
ImageMath.unsafe_eval("(lambda: exec('exit()'))()", exec=None)
def test_logical() -> None:
assert pixel(ImageMath.unsafe_eval("not A", images)) == 0
assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1"
assert pixel(ImageMath.unsafe_eval("not A", **images)) == 0
assert pixel(ImageMath.unsafe_eval("A and B", **images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A or B", **images)) == "L 1"
def test_convert() -> None:
assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3"
assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0"
assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", **images)) == "L 3"
assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", **images)) == "1 0"
assert (
pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)"
pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", **images)) == "RGB (3, 3, 3)"
)
def test_compare() -> None:
assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("min(A, B)", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("max(A, B)", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A == 1", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("A == 2", **images)) == "I 0"
def test_one_image_larger() -> None:

View File

@ -41,11 +41,16 @@ A = string_to_img(
def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation"""
chars = ".1"
width, height = im.size
return "\n".join(
"".join(chars[im.getpixel((c, r)) > 0] for c in range(width))
for r in range(height)
)
result = []
for r in range(im.height):
line = ""
for c in range(im.width):
value = im.getpixel((c, r))
assert not isinstance(value, tuple)
assert value is not None
line += chars[value > 0]
result.append(line)
return "\n".join(result)
def img_string_normalize(im: str) -> str:

View File

@ -259,20 +259,26 @@ def test_colorize_2color() -> None:
left = (0, 1)
middle = (127, 1)
right = (255, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle),
value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -295,20 +301,26 @@ def test_colorize_2color_offset() -> None:
left = (25, 1)
middle = (75, 1)
right = (125, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle),
value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -339,29 +351,37 @@ def test_colorize_3color_offset() -> None:
middle = (100, 1)
right_middle = (150, 1)
right = (225, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(left_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left_middle),
value,
(127, 0, 127),
threshold=1,
msg="low-mid test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
value = im_test.getpixel(right_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect"
)
assert_tuple_approx_equal(
im_test.getpixel(right_middle),
value,
(0, 63, 127),
threshold=1,
msg="high-mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -370,7 +390,7 @@ def test_colorize_3color_offset() -> None:
def test_exif_transpose() -> None:
exts = [".jpg"]
if features.check("webp") and features.check("webp_anim"):
if features.check("webp"):
exts.append(".webp")
for ext in exts:
with Image.open("Tests/images/hopper" + ext) as base_im:
@ -444,6 +464,7 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()

View File

@ -16,8 +16,11 @@ def test_sanity() -> None:
def test_register() -> None:
# Test registering a viewer that is not a class
ImageShow.register("not a class")
# Test registering a viewer that is an instance
class TestViewer(ImageShow.Viewer):
pass
ImageShow.register(TestViewer())
# Restore original state
ImageShow._viewers.pop()

View File

@ -45,10 +45,12 @@ def test_kw() -> None:
# Test "file"
im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im1)
# Test "data"
im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im2)
# Test no relevant entry
@ -107,3 +109,6 @@ def test_bitmapimage() -> None:
# reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im)
with pytest.raises(ValueError):
ImageTk.BitmapImage()

View File

@ -57,6 +57,9 @@ class TestImageWinDib:
# Assert
assert dib.size == (128, 128)
with pytest.raises(ValueError):
ImageWin.Dib(mode)
def test_dib_paste(self) -> None:
# Arrange
im = hopper()

View File

@ -198,6 +198,15 @@ def test_putdata() -> None:
assert len(im.getdata()) == len(arr)
def test_resize() -> None:
im = hopper()
size = (64, 64)
im_resized = im.resize(numpy.array(size))
assert im_resized.size == size
@pytest.mark.parametrize(
"dtype",
(

View File

@ -46,7 +46,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
@pytest.mark.parametrize(
("test_file", "test_mode"),
"test_file, test_mode",
[
("Tests/images/hopper.jpg", None),
("Tests/images/hopper.jpg", "L"),

View File

@ -5,8 +5,6 @@ import sys
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, PSDraw
@ -49,17 +47,16 @@ def test_draw_postscript(tmp_path: Path) -> None:
assert os.path.getsize(tempfile) > 0
@pytest.mark.parametrize("buffer", (True, False))
def test_stdout(buffer: bool) -> None:
def test_stdout() -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
mystdout = MyStdOut()
sys.stdout = mystdout # type: ignore[assignment]
sys.stdout = mystdout
ps = PSDraw.PSDraw()
_create_document(ps)
@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""
assert mystdout.buffer.getvalue() != b""

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Union
import pytest
@ -8,6 +9,20 @@ from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
if TYPE_CHECKING:
import PyQt6
import PySide6
QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@ -20,7 +35,7 @@ if ImageQt.qt_is_installed:
from PySide6.QtGui import QImage, QPainter, QRegion
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
class Example(QWidget):
class Example(QWidget): # type: ignore[misc]
def __init__(self) -> None:
super().__init__()
@ -28,11 +43,12 @@ if ImageQt.qt_is_installed:
qimage = ImageQt.ImageQt(img)
pixmap1 = ImageQt.QPixmap.fromImage(qimage)
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
QHBoxLayout(self) # hbox
# hbox
QHBoxLayout(self) # type: ignore[operator]
lbl = QLabel(self)
lbl = QLabel(self) # type: ignore[operator]
# Segfault in the problem
lbl.setPixmap(pixmap1.copy())
@ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None:
# Segfault test
app: QApplication | None = QApplication([])
app: QApplication | None = QApplication([]) # type: ignore[operator]
ex = Example()
assert app # Silence warning
assert ex # Silence warning
@ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None:
im = hopper(mode)
data = ImageQt.toqpixmap(im)
assert isinstance(data, QPixmap)
assert data.__class__.__name__ == "QPixmap"
assert not data.isNull()
# Test saving the file
@ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None:
data.save(tempfile)
# Render the image
qimage = ImageQt.ImageQt(im)
data = QPixmap.fromImage(qimage)
qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, qt_format.Format_ARGB32)
painter = QPainter(qimage)
image_label = QLabel()
imageqt = ImageQt.ImageQt(im)
data = getattr(QPixmap, "fromImage")(imageqt)
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
painter = QPainter(qimage) # type: ignore[operator]
image_label = QLabel() # type: ignore[operator]
image_label.setPixmap(data)
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
painter.end()
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
qimage.save(rendered_tempfile)

View File

@ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
assert isinstance(data, QImage)
assert isinstance(data, QImage) # type: ignore[arg-type, misc]
assert not data.isNull()
# reload directly from the qimage

View File

@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import hopper, skip_unless_feature
def _test_equal(num, denom, target) -> None:
def _test_equal(
num: float | Fraction | IFDRational,
denom: int,
target: float | Fraction | IFDRational,
) -> None:
t = IFDRational(num, denom)
assert target == t
@ -50,8 +54,8 @@ def test_nonetype() -> None:
assert xres.denominator is not None
assert yres._val is not None
assert xres and 1
assert xres and yres
assert xres
assert yres
@pytest.mark.parametrize(

View File

@ -30,28 +30,6 @@ def test_is_not_path(tmp_path: Path) -> None:
assert not it_is_not
def test_is_directory() -> None:
# Arrange
directory = "Tests"
# Act
it_is = _util.is_directory(directory)
# Assert
assert it_is
def test_is_not_directory() -> None:
# Arrange
text = "abc"
# Act
it_is_not = _util.is_directory(text)
# Assert
assert not it_is_not
def test_deferred_error() -> None:
# Arrange

View File

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.1
archive_version=4.3.3
archive=$archive_name-$archive_version

View File

@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH').
nitpick_ignore = [("py:class", "_CmsProfileCompatible")]
nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
# -- Options for HTML output ----------------------------------------------

View File

@ -109,12 +109,46 @@ ImageDraw.getdraw hints parameter
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword
arguments can be used instead.
JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They
have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow
12.0.0 (2025-10-15).
Removed features
----------------
Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed.
TiffImagePlugin IFD_LEGACY_API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 11.0.0
``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting.
PSFile
~~~~~~

View File

@ -14,6 +14,7 @@ from __future__ import annotations
import struct
from io import BytesIO
from typing import IO
from PIL import Image, ImageFile
@ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844
DXT5_FOURCC = 0x35545844
def _decode565(bits):
def _decode565(bits: int) -> tuple[int, int, int]:
a = ((bits >> 11) & 0x1F) << 3
b = ((bits >> 5) & 0x3F) << 2
c = (bits & 0x1F) << 3
return a, b, c
def _c2a(a, b):
def _c2a(a: int, b: int) -> int:
return (2 * a + b) // 3
def _c2b(a, b):
def _c2b(a: int, b: int) -> int:
return (a + b) // 2
def _c3(a, b):
def _c3(a: int, b: int) -> int:
return (2 * b + a) // 3
def _dxt1(data, width, height):
def _dxt1(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height)
@ -151,7 +152,7 @@ def _dxt1(data, width, height):
return bytes(ret)
def _dxtc_alpha(a0, a1, ac0, ac1, ai):
def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int:
if ai <= 12:
ac = (ac0 >> ai) & 7
elif ai == 15:
@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai):
return alpha
def _dxt5(data, width, height):
def _dxt5(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height)
@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS"
format_description = "DirectDraw Surface"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(4)):
msg = "not a DDS file"
raise SyntaxError(msg)
@ -242,19 +243,22 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"DXT5":
self.decoder = "DXT5"
else:
msg = f"Unimplemented pixel format {fourcc}"
msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg)
self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
self.tile = [
ImageFile._Tile(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
]
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
pass
class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e:
@ -266,7 +270,8 @@ class DXT1Decoder(ImageFile.PyDecoder):
class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e:
@ -279,7 +284,7 @@ Image.register_decoder("DXT1", DXT1Decoder)
Image.register_decoder("DXT5", DXT5Decoder)
def _accept(prefix):
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS "

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1220,8 +1220,7 @@ using the general tags available through tiffinfo.
WebP
^^^^
Pillow reads and writes WebP files. The specifics of Pillow's capabilities with
this format are currently undocumented.
Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later.
.. _webp-saving:
@ -1249,29 +1248,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exact**
If true, preserve the transparent RGB values. Otherwise, discard
invisible RGB values for better compression. Defaults to false.
Requires libwebp 0.5.0 or later.
**icc_profile**
The ICC Profile to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
The ICC Profile to include in the saved file.
**exif**
The exif data to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
The exif data to include in the saved file.
**xmp**
The XMP data to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
The XMP data to include in the saved file.
Saving sequences
~~~~~~~~~~~~~~~~
.. note::
Support for animated WebP files will only be enabled if the system WebP
library is v0.5.0 or later. You can check webp animation support at
runtime by calling ``features.check("webp_anim")``.
When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
@ -1528,19 +1517,21 @@ To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler. ::
from PIL import Image
from typing import IO
from PIL import Image, ImageFile
from PIL import WmfImagePlugin
class WmfHandler:
def open(self, im):
class WmfHandler(ImageFile.StubHandler):
def open(self, im: ImageFile.StubImageFile) -> None:
...
def load(self, im):
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
...
return image
def save(self, im, fp, filename):
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
...

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -37,6 +37,9 @@ example, lets display the image we just loaded::
>>> im.show()
.. image:: show_hopper.webp
:align: center
.. note::
The standard version of :py:meth:`~PIL.Image.Image.show` is not very
@ -79,6 +82,9 @@ Convert files to JPEG
except OSError:
print("cannot convert", infile)
.. image:: ../../Tests/images/hopper.jpg
:align: center
A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save`
method which explicitly specifies a file format. If you use a non-standard
extension, you must always specify the format this way:
@ -103,6 +109,9 @@ Create JPEG thumbnails
except OSError:
print("cannot create thumbnail for", infile)
.. image:: thumbnail_hopper.jpg
:align: center
It is important to note that the library doesnt decode or load the raster data
unless it really has to. When you open a file, the file header is read to
determine the file format and extract things like mode, size, and other
@ -140,16 +149,19 @@ Copying a subrectangle from an image
::
box = (100, 100, 400, 400)
box = (0, 0, 64, 64)
region = im.crop(box)
The region is defined by a 4-tuple, where coordinates are (left, upper, right,
lower). The Python Imaging Library uses a coordinate system with (0, 0) in the
upper left corner. Also note that coordinates refer to positions between the
pixels, so the region in the above example is exactly 300x300 pixels.
pixels, so the region in the above example is exactly 64x64 pixels.
The region could now be processed in a certain manner and pasted back.
.. image:: cropped_hopper.webp
:align: center
Processing a subrectangle, and pasting it back
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -164,6 +176,9 @@ modes of the original image and the region do not need to match. If they dont
the region is automatically converted before being pasted (see the section on
:ref:`color-transforms` below for details).
.. image:: pasted_hopper.webp
:align: center
Heres an additional example:
Rolling an image
@ -171,7 +186,7 @@ Rolling an image
::
def roll(im, delta):
def roll(im: Image.Image, delta: int) -> Image.Image:
"""Roll an image sideways."""
xsize, ysize = im.size
@ -186,6 +201,9 @@ Rolling an image
return im
.. image:: rolled_hopper.webp
:align: center
Or if you would like to merge two images into a wider image:
Merging images
@ -193,7 +211,7 @@ Merging images
::
def merge(im1, im2):
def merge(im1: Image.Image, im2: Image.Image) -> Image.Image:
w = im1.size[0] + im2.size[0]
h = max(im1.size[1], im2.size[1])
im = Image.new("RGBA", (w, h))
@ -203,6 +221,9 @@ Merging images
return im
.. image:: merged_hopper.webp
:align: center
For more advanced tricks, the paste method can also take a transparency mask as
an optional argument. In this mask, the value 255 indicates that the pasted
image is opaque in that position (that is, the pasted image should be used as
@ -229,6 +250,9 @@ Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns
the image itself. To work with individual color bands, you may want to convert
the image to “RGB” first.
.. image:: rebanded_hopper.webp
:align: center
Geometrical transforms
----------------------
@ -245,6 +269,9 @@ Simple geometry transforms
out = im.resize((128, 128))
out = im.rotate(45) # degrees counter-clockwise
.. image:: rotated_hopper_90.webp
:align: center
To rotate the image in 90 degree steps, you can either use the
:py:meth:`~PIL.Image.Image.rotate` method or the
:py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to
@ -256,11 +283,38 @@ Transposing an image
::
out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
.. image:: flip_left_right_hopper.webp
:align: center
::
out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
.. image:: flip_top_bottom_hopper.webp
:align: center
::
out = im.transpose(Image.Transpose.ROTATE_90)
.. image:: rotated_hopper_90.webp
:align: center
::
out = im.transpose(Image.Transpose.ROTATE_180)
.. image:: rotated_hopper_180.webp
:align: center
::
out = im.transpose(Image.Transpose.ROTATE_270)
.. image:: rotated_hopper_270.webp
:align: center
``transpose(ROTATE)`` operations can also be performed identically with
:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is
true, to provide for the same changes to the image's size.
@ -278,7 +332,7 @@ choose to resize relative to a given size.
from PIL import Image, ImageOps
size = (100, 150)
with Image.open("Tests/images/hopper.webp") as im:
with Image.open("hopper.webp") as im:
ImageOps.contain(im, size).save("imageops_contain.webp")
ImageOps.cover(im, size).save("imageops_cover.webp")
ImageOps.fit(im, size).save("imageops_fit.webp")
@ -342,6 +396,9 @@ Applying filters
from PIL import ImageFilter
out = im.filter(ImageFilter.DETAIL)
.. image:: enhanced_hopper.webp
:align: center
Point Operations
^^^^^^^^^^^^^^^^
@ -355,8 +412,11 @@ Applying point transforms
::
# multiply each pixel by 1.2
out = im.point(lambda i: i * 1.2)
# multiply each pixel by 20
out = im.point(lambda i: i * 20)
.. image:: transformed_hopper.webp
:align: center
Using the above technique, you can quickly apply any simple expression to an
image. You can also combine the :py:meth:`~PIL.Image.Image.point` and
@ -388,6 +448,9 @@ Note the syntax used to create the mask::
imout = im.point(lambda i: expression and 255)
.. image:: masked_hopper.webp
:align: center
Python only evaluates the portion of a logical expression as is necessary to
determine the outcome, and returns the last value examined as the result of the
expression. So if the expression above is false (0), Python does not look at
@ -412,6 +475,10 @@ Enhancing images
enh = ImageEnhance.Contrast(im)
enh.enhance(1.3).show("30% more contrast")
.. image:: contrasted_hopper.jpg
:align: center
Image sequences
---------------
@ -444,10 +511,43 @@ Reading sequences
As seen in this example, youll get an :py:exc:`EOFError` exception when the
sequence ends.
Writing sequences
^^^^^^^^^^^^^^^^^
You can create animated GIFs with Pillow, e.g.
::
from PIL import Image
# List of image filenames
image_filenames = [
"hopper.jpg",
"rotated_hopper_270.jpg",
"rotated_hopper_180.jpg",
"rotated_hopper_90.jpg",
]
# Open images and create a list
images = [Image.open(filename) for filename in image_filenames]
# Save the images as an animated GIF
images[0].save(
"animated_hopper.gif",
save_all=True,
append_images=images[1:],
duration=500, # duration of each frame in milliseconds
loop=0, # loop forever
)
.. image:: animated_hopper.gif
:align: center
The following class lets you use the for-statement to loop over the sequence:
Using the ImageSequence Iterator class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Using the :py:class:`~PIL.ImageSequence.Iterator` class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
::
@ -467,25 +567,61 @@ Drawing PostScript
::
from PIL import Image
from PIL import PSDraw
from PIL import Image, PSDraw
import os
with Image.open("hopper.ppm") as im:
title = "hopper"
box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points
# Define the PostScript file
ps_file = open("hopper.ps", "wb")
ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer
ps.begin_document(title)
# Create a PSDraw object
ps = PSDraw.PSDraw(ps_file)
# draw the image (75 dpi)
ps.image(box, im, 75)
ps.rectangle(box)
# Start the document
ps.begin_document()
# draw title
ps.setfont("HelveticaNarrow-Bold", 36)
ps.text((3 * 72, 4 * 72), title)
# Set the text to be drawn
text = "Hopper"
# Define the PostScript font
font_name = "Helvetica-Narrow-Bold"
font_size = 36
# Calculate text size (approximation as PSDraw doesn't provide direct method)
# Assuming average character width as 0.6 of the font size
text_width = len(text) * font_size * 0.6
text_height = font_size
# Set the position (top-center)
page_width, page_height = 595, 842 # A4 size in points
text_x = (page_width - text_width) // 2
text_y = page_height - text_height - 50 # Distance from the top of the page
# Load the image
image_path = "hopper.ppm" # Update this with your image path
with Image.open(image_path) as im:
# Resize the image if it's too large
im.thumbnail((page_width - 100, page_height // 2))
# Define the box where the image will be placed
img_x = (page_width - im.width) // 2
img_y = text_y + text_height - 200 # 200 points below the text
# Draw the image (75 dpi)
ps.image((img_x, img_y, img_x + im.width, img_y + im.height), im, 75)
# Draw the text
ps.setfont(font_name, font_size)
ps.text((text_x, text_y), text)
# End the document
ps.end_document()
ps_file.close()
.. image:: hopper_ps.webp
.. note::
PostScript converted to PDF for display purposes
More on reading images
----------------------
@ -553,7 +689,7 @@ Reading from a tar archive
from PIL import Image, TarIO
fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg")
fp = TarIO.TarIO("hopper.tar", "hopper.jpg")
im = Image.open(fp)
@ -568,8 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality.
import glob
from PIL import Image
def compress_image(source_path, dest_path):
def compress_image(source_path: str, dest_path: str) -> None:
with Image.open(source_path) as img:
if img.mode != "RGB":
img = img.convert("RGB")

View File

@ -53,7 +53,7 @@ true color.
from PIL import Image, ImageFile
def _accept(prefix):
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM"
@ -62,7 +62,7 @@ true color.
format = "SPAM"
format_description = "Spam raster image"
def _open(self):
def _open(self) -> None:
header = self.fp.read(128).split()
@ -82,7 +82,7 @@ true color.
raise SyntaxError(msg)
# data descriptor
self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]
Image.register_open(SpamImageFile.format, SpamImageFile, _accept)

Some files were not shown because too many files have changed in this diff Show More