Merge branch 'main' into convert_mode

This commit is contained in:
Andrew Murray 2022-06-14 22:54:21 +10:00 committed by GitHub
commit 38e7d9b46c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
162 changed files with 3509 additions and 2207 deletions

View File

@ -3,7 +3,7 @@
# gather the coverage data
python3 -m pip install codecov
if [[ $MATRIX_DOCKER ]]; then
coverage xml --ignore-errors
python3 -m coverage xml --ignore-errors
else
coverage xml
python3 -m coverage xml
fi

View File

@ -2,7 +2,7 @@
set -e
coverage erase
python3 -m coverage erase
if [ $(uname) == "Darwin" ]; then
export CPPFLAGS="-I/usr/local/miniconda/include";
fi

View File

@ -13,13 +13,17 @@ aptget_update()
return 1
fi
}
if [[ $(uname) != CYGWIN* ]]; then
aptget_update || aptget_update retry || aptget_update retry
fi
set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
fi
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
@ -32,14 +36,15 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
python3 -m pip install test-image-results
python3 -m pip install numpy
# PyQt5 doesn't support PyPy3
if [[ $(uname) != CYGWIN* ]]; then
# TODO Remove condition when NumPy supports 3.11
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
# arm64, ppc64le, s390x CPUs:
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
python3 -m pip install pyqt5
sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0
python3 -m pip install pyqt6
fi
# webp
@ -53,3 +58,6 @@ pushd depends && ./install_raqm.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
else
cd depends && ./install_extra_test_images.sh && cd ..
fi

View File

@ -16,7 +16,6 @@ trim_trailing_whitespace = true
[*.yml]
# Two-space indentation
indent_size = 2
indent_style = space
# Tab indentation (no size specified)
[Makefile]

View File

@ -4,7 +4,7 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
## Bug fixes, feature additions, etc.
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
- Fork the Pillow repository.
- Create a branch from `main`.

1
.github/mergify.yml vendored
View File

@ -8,6 +8,7 @@ pull_request_rules:
- status-success=Docker Test Successful
- status-success=Windows Test Successful
- status-success=MinGW Test Successful
- status-success=Cygwin Test Successful
- status-success=continuous-integration/appveyor/pr
actions:
merge:

View File

@ -15,7 +15,8 @@ python3 -m pip install pyroma
python3 -m pip install test-image-results
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
python3 -m pip install numpy
# TODO Remove condition when NumPy supports 3.11
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

27
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Close stale issues
on:
schedule:
- cron: "10 0 * * *"
workflow_dispatch:
permissions:
issues: write
jobs:
stale:
if: github.repository_owner == 'python-pillow'
runs-on: ubuntu-latest
steps:
- name: "Check issues"
uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"
close-issue-message: "Closing this issue as no feedback has been received."
days-before-stale: 7
days-before-issue-close: 0
days-before-pr-close: -1
labels-to-remove-when-unstale: "Awaiting OP Action"

107
.github/workflows/test-cygwin.yml vendored Normal file
View File

@ -0,0 +1,107 @@
name: Test Cygwin
on: [push, pull_request, workflow_dispatch]
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
python-minor-version: [7, 8, 9]
timeout-minutes: 40
name: Python 3.${{ matrix.python-minor-version }}
steps:
- name: Fix line endings
run: |
git config --global core.autocrlf input
- name: Checkout Pillow
uses: actions/checkout@v3
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v2
with:
platform: x86_64
packages: >
ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel
libimagequant-devel libjpeg-devel liblapack-devel
liblcms2-devel libopenjp2-devel libraqm-devel
libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0
make netpbm perl
python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter
qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v1
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
- name: pip cache
uses: actions/cache@v3
with:
path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
restore-keys: |
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
- name: Build system information
run: |
dash.exe -c "python3 .github/workflows/system-info.py"
- name: Install dependencies
run: |
bash.exe .ci/install.sh
- name: Install a different NumPy
shell: dash.exe -l "{0}"
run: |
python3 -m pip install -U 'numpy!=1.21.*'
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
.ci/build.sh
- name: Test
run: |
bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
- name: Prepare to upload errors
if: failure()
run: |
dash.exe -c "mkdir -p Tests/errors"
- name: Upload errors
uses: actions/upload-artifact@v3
if: failure()
with:
name: errors
path: Tests/errors
- name: After success
run: |
bash.exe .ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }}
success:
needs: build
runs-on: ubuntu-latest
name: Cygwin Test Successful
steps:
- name: Success
run: echo Cygwin Test Successful

View File

@ -11,9 +11,9 @@ jobs:
matrix:
docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-20.04-focal-arm64v8,
ubuntu-20.04-focal-ppc64le,
ubuntu-20.04-focal-s390x,
ubuntu-22.04-jammy-arm64v8,
ubuntu-22.04-jammy-ppc64le,
ubuntu-22.04-jammy-s390x,
# Then run the remainder
alpine,
amazon-2-amd64,
@ -24,17 +24,19 @@ jobs:
debian-10-buster-x86,
debian-11-bullseye-x86,
fedora-35-amd64,
fedora-36-amd64,
gentoo,
ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
]
dockerTag: [main]
include:
- docker: "ubuntu-20.04-focal-arm64v8"
- docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64"
- docker: "ubuntu-20.04-focal-ppc64le"
- docker: "ubuntu-22.04-jammy-ppc64le"
qemu-arch: "ppc64le"
- docker: "ubuntu-20.04-focal-s390x"
- docker: "ubuntu-22.04-jammy-s390x"
qemu-arch: "s390x"
name: ${{ matrix.docker }}

View File

@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
architecture: ["x86", "x64"]
include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
@ -41,10 +41,10 @@ jobs:
cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information
run: python .github/workflows/system-info.py
run: python3 .github/workflows/system-info.py
- name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
- name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
- name: Install dependencies
id: install

View File

@ -15,6 +15,7 @@ jobs:
python-version: [
"pypy-3.8",
"pypy-3.7",
"3.11-dev",
"3.10",
"3.9",
"3.8",
@ -59,6 +60,8 @@ jobs:
if: startsWith(matrix.os, 'macOS')
run: |
.github/workflows/macos-install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Build
run: |

View File

@ -4,9 +4,11 @@ on:
- cron: "30 2 * * *" # daily at 02:30 UTC
push:
paths:
- "Pipfile*"
- ".github/workflows/tidelift.yml"
pull_request:
paths:
- "Pipfile*"
- ".github/workflows/tidelift.yml"
workflow_dispatch:

View File

@ -19,7 +19,7 @@ repos:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.1.13
rev: v1.2.0
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
@ -37,10 +37,15 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.2.0
hooks:
- id: check-merge-conflict
- id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6
hooks:
- id: sphinx-lint
ci:
autoupdate_schedule: quarterly
autoupdate_schedule: monthly

File diff suppressed because it is too large Load Diff

View File

@ -85,6 +85,8 @@ release-test:
sdist:
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
python3 -m build --sdist
python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
python3 -m twine check --strict dist/*
.PHONY: test
test:

View File

@ -36,6 +36,9 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
alt="GitHub Actions build status (Test MinGW)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
alt="GitHub Actions build status (Test Cygwin)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>

View File

@ -24,7 +24,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create and check source distribution:
```bash
make sdist
python3 -m twine check --strict dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
@ -61,7 +60,6 @@ Released as needed for security, installation or critical bug fixes.
* [ ] Create and check source distribution:
```bash
make sdist
python3 -m twine check --strict dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Create and check source distribution:
```bash
make sdist
python3 -m twine check --strict dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)

View File

@ -8,7 +8,7 @@ Dependencies
Install::
python3 -m pip install pytest pytest-cov
python3 -m pip install pytest pytest-cov pytest-timeout
Execution
---------

View File

@ -324,7 +324,7 @@ def is_mingw():
return sysconfig.get_platform() == "mingw"
class cached_property:
class CachedProperty:
def __init__(self, func):
self.func = func

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 B

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

BIN
Tests/images/issue_6194.j2k Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

BIN
Tests/images/tiny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

View File

@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1):
return image._new(image.im.box_blur(radius, n))
def assertImage(im, data, delta=0):
def assert_image(im, data, delta=0):
it = iter(im.getdata())
for data_row in data:
im_row = [next(it) for _ in range(im.size[0])]
@ -35,12 +35,12 @@ def assertImage(im, data, delta=0):
next(it)
def assertBlur(im, radius, data, passes=1, delta=0):
def assert_blur(im, radius, data, passes=1, delta=0):
# check grayscale image
assertImage(box_blur(im, radius, passes), data, delta)
assert_image(box_blur(im, radius, passes), data, delta)
rgba = Image.merge("RGBA", (im, im, im, im))
for band in box_blur(rgba, radius, passes).split():
assertImage(band, data, delta)
assert_image(band, data, delta)
def test_color_modes():
@ -64,7 +64,7 @@ def test_color_modes():
def test_radius_0():
assertBlur(
assert_blur(
sample,
0,
[
@ -80,7 +80,7 @@ def test_radius_0():
def test_radius_0_02():
assertBlur(
assert_blur(
sample,
0.02,
[
@ -97,7 +97,7 @@ def test_radius_0_02():
def test_radius_0_05():
assertBlur(
assert_blur(
sample,
0.05,
[
@ -114,7 +114,7 @@ def test_radius_0_05():
def test_radius_0_1():
assertBlur(
assert_blur(
sample,
0.1,
[
@ -131,7 +131,7 @@ def test_radius_0_1():
def test_radius_0_5():
assertBlur(
assert_blur(
sample,
0.5,
[
@ -148,7 +148,7 @@ def test_radius_0_5():
def test_radius_1():
assertBlur(
assert_blur(
sample,
1,
[
@ -165,7 +165,7 @@ def test_radius_1():
def test_radius_1_5():
assertBlur(
assert_blur(
sample,
1.5,
[
@ -182,7 +182,7 @@ def test_radius_1_5():
def test_radius_bigger_then_half():
assertBlur(
assert_blur(
sample,
3,
[
@ -199,7 +199,7 @@ def test_radius_bigger_then_half():
def test_radius_bigger_then_width():
assertBlur(
assert_blur(
sample,
10,
[
@ -214,7 +214,7 @@ def test_radius_bigger_then_width():
def test_extreme_large_radius():
assertBlur(
assert_blur(
sample,
600,
[
@ -229,7 +229,7 @@ def test_extreme_large_radius():
def test_two_passes():
assertBlur(
assert_blur(
sample,
1,
[
@ -247,7 +247,7 @@ def test_two_passes():
def test_three_passes():
assertBlur(
assert_blur(
sample,
1,
[

View File

@ -15,27 +15,27 @@ except ImportError:
class TestColorLut3DCoreAPI:
def generate_identity_table(self, channels, size):
if isinstance(size, tuple):
size1D, size2D, size3D = size
size_1d, size_2d, size_3d = size
else:
size1D, size2D, size3D = (size, size, size)
size_1d, size_2d, size_3d = (size, size, size)
table = [
[
r / (size1D - 1) if size1D != 1 else 0,
g / (size2D - 1) if size2D != 1 else 0,
b / (size3D - 1) if size3D != 1 else 0,
r / (size1D - 1) if size1D != 1 else 0,
g / (size2D - 1) if size2D != 1 else 0,
r / (size_1d - 1) if size_1d != 1 else 0,
g / (size_2d - 1) if size_2d != 1 else 0,
b / (size_3d - 1) if size_3d != 1 else 0,
r / (size_1d - 1) if size_1d != 1 else 0,
g / (size_2d - 1) if size_2d != 1 else 0,
][:channels]
for b in range(size3D)
for g in range(size2D)
for r in range(size1D)
for b in range(size_3d)
for g in range(size_2d)
for r in range(size_1d)
]
return (
channels,
size1D,
size2D,
size3D,
size_1d,
size_2d,
size_3d,
[item for sublist in table for item in sublist],
)
@ -567,7 +567,7 @@ class TestTransformColorLut3D:
assert tuple(lut.size) == tuple(source.size)
assert len(lut.table) == len(source.table)
assert lut.table != source.table
assert lut.table[0:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
def test_3_to_4_channels(self):
source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
@ -576,7 +576,7 @@ class TestTransformColorLut3D:
assert len(lut.table) != len(source.table)
assert lut.table != source.table
# fmt: off
assert lut.table[0:16] == [
assert lut.table[:16] == [
0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1,
0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]
# fmt: on
@ -592,7 +592,7 @@ class TestTransformColorLut3D:
assert len(lut.table) != len(source.table)
assert lut.table != source.table
# fmt: off
assert lut.table[0:18] == [
assert lut.table[:18] == [
1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0,
1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]
# fmt: on
@ -606,7 +606,7 @@ class TestTransformColorLut3D:
assert len(lut.table) == len(source.table)
assert lut.table != source.table
# fmt: off
assert lut.table[0:16] == [
assert lut.table[:16] == [
0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5,
0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]
# fmt: on
@ -622,7 +622,7 @@ class TestTransformColorLut3D:
assert len(lut.table) == len(source.table)
assert lut.table != source.table
# fmt: off
assert lut.table[0:18] == [
assert lut.table[:18] == [
0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0,
0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]
# fmt: on
@ -639,7 +639,7 @@ class TestTransformColorLut3D:
assert len(lut.table) == len(source.table)
assert lut.table != source.table
# fmt: off
assert lut.table[0:16] == [
assert lut.table[:16] == [
0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5,
0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5]
# fmt: on

View File

@ -51,7 +51,6 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE):
pass
@pytest.mark.xfail(reason="different exception")
def test_exception_ico(self):
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.ico"):
@ -70,15 +69,15 @@ class TestDecompressionBomb:
class TestDecompressionCrop:
@classmethod
def setup_class(self):
def setup_class(cls):
width, height = 128, 128
Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
@classmethod
def teardown_class(self):
def teardown_class(cls):
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def testEnlargeCrop(self):
def test_enlarge_crop(self):
# Crops can extend the extents, therefore we should have the
# same decompression bomb warnings on them.
with hopper() as src:

91
Tests/test_deprecate.py Normal file
View File

@ -0,0 +1,91 @@
import pytest
from PIL import _deprecate
@pytest.mark.parametrize(
"version, expected",
[
(
10,
"Old thing is deprecated and will be removed in Pillow 10 "
r"\(2023-07-01\)\. Use new thing instead\.",
),
(
None,
r"Old thing is deprecated and will be removed in a future version\. "
r"Use new thing instead\.",
),
],
)
def test_version(version, expected):
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing")
def test_unknown_version():
expected = r"Unknown removal version, update PIL\._deprecate\?"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate("Old thing", 12345, "new thing")
@pytest.mark.parametrize(
"deprecated, plural, expected",
[
(
"Old thing",
False,
r"Old thing is deprecated and should be removed\.",
),
(
"Old things",
True,
r"Old things are deprecated and should be removed\.",
),
],
)
def test_old_version(deprecated, plural, expected):
expected = r""
with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural)
def test_plural():
expected = (
r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
r"Use new thing instead\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old things", 10, "new thing", plural=True)
def test_replacement_and_action():
expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate(
"Old thing", 10, replacement="new thing", action="Upgrade to new thing"
)
@pytest.mark.parametrize(
"action",
[
"Upgrade to new thing",
"Upgrade to new thing.",
],
)
def test_action(action):
expected = (
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
r"Upgrade to new thing\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 10, action=action)
def test_no_replacement_or_action():
expected = (
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)"
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 10)

View File

@ -0,0 +1,18 @@
import warnings
with warnings.catch_warnings(record=True) as w:
# Arrange: cause all warnings to always be triggered
warnings.simplefilter("always")
# Act: trigger a warning with Qt5
from PIL import ImageQt
def test_deprecated():
# Assert
if ImageQt.qt_version in ("5", "side2"):
assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "deprecated" in str(w[0].message)
else:
assert len(w) == 0

View File

@ -637,6 +637,15 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
def test_seek_after_close():
im = Image.open("Tests/images/apng/delay.png")
im.seek(1)
im.close()
with pytest.raises(ValueError):
im.seek(0)
def test_constants_deprecation():
for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_",

View File

@ -46,6 +46,15 @@ def test_closed_file():
im.close()
def test_seek_after_close():
im = Image.open(animated_test_file)
im.seek(1)
im.close()
with pytest.raises(ValueError):
im.seek(0)
def test_context_manager():
with warnings.catch_warnings():
with Image.open(static_test_file) as im:

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features
from .helper import (
assert_image_equal,
@ -46,6 +46,19 @@ def test_closed_file():
im.close()
def test_seek_after_close():
im = Image.open("Tests/images/iss634.gif")
im.load()
im.close()
with pytest.raises(ValueError):
im.is_animated
with pytest.raises(ValueError):
im.n_frames
with pytest.raises(ValueError):
im.seek(1)
def test_context_manager():
with warnings.catch_warnings():
with Image.open(TEST_GIF) as im:
@ -341,8 +354,15 @@ def test_seek_rewind():
assert_image_equal(im, expected)
def test_n_frames():
for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]:
@pytest.mark.parametrize(
"path, n_frames",
(
(TEST_GIF, 1),
("Tests/images/comment_after_last_frame.gif", 2),
("Tests/images/iss634.gif", 42),
),
)
def test_n_frames(path, n_frames):
# Test is_animated before n_frames
with Image.open(path) as im:
assert im.is_animated == (n_frames != 1)
@ -619,7 +639,8 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0)
def test_transparency_in_second_frame():
def test_transparency_in_second_frame(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0
@ -629,6 +650,14 @@ def test_transparency_in_second_frame():
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
im.save(out, save_all=True)
with Image.open(out) as reread:
reread.seek(reread.tell() + 1)
assert_image_equal_tofile(
reread, "Tests/images/different_transparency_merged.png"
)
def test_no_transparency_in_second_frame():
with Image.open("Tests/images/iss634.gif") as img:
@ -640,6 +669,22 @@ def test_no_transparency_in_second_frame():
assert img.histogram()[255] == 0
def test_remapped_transparency(tmp_path):
out = str(tmp_path / "temp.gif")
im = Image.new("P", (1, 2))
im2 = im.copy()
# Add transparency at a higher index
# so that it will be optimized to a lower index
im.putpixel((0, 1), 5)
im.info["transparency"] = 5
im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reloaded:
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
def test_duration(tmp_path):
duration = 1000
@ -691,6 +736,23 @@ def test_multiple_duration(tmp_path):
pass
def test_roundtrip_info_duration(tmp_path):
duration_list = [100, 500, 500]
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/transparent_dispose.gif") as im:
assert [
frame.info["duration"] for frame in ImageSequence.Iterator(im)
] == duration_list
im.save(out, save_all=True)
with Image.open(out) as reloaded:
assert [
frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
] == duration_list
def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000]
@ -742,9 +804,16 @@ def test_number_of_loops(tmp_path):
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops)
with Image.open(out) as reread:
assert reread.info["loop"] == number_of_loops
# Check that even if a subsequent GIF frame has the number of loops specified,
# only the value from the first frame is used
with Image.open("Tests/images/duplicate_number_of_loops.gif") as im:
assert im.info["loop"] == 2
im.seek(1)
assert im.info["loop"] == 2
def test_background(tmp_path):
out = str(tmp_path / "temp.gif")
@ -777,6 +846,9 @@ def test_comment(tmp_path):
with Image.open(out) as reread:
assert reread.info["comment"] == im.info["comment"].encode()
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
def test_comment_over_255(tmp_path):
out = str(tmp_path / "temp.gif")
@ -787,43 +859,95 @@ def test_comment_over_255(tmp_path):
im.info["comment"] = comment
im.save(out)
with Image.open(out) as reread:
assert reread.info["comment"] == comment
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
def test_zero_comment_subblocks():
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
assert_image_equal_tofile(im, TEST_GIF)
def test_read_multiple_comment_blocks():
with Image.open("Tests/images/multiple_comments.gif") as im:
# Multiple comment blocks in a frame are separated not concatenated
assert im.info["comment"] == b"Test comment 1\nTest comment 2"
def test_empty_string_comment(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info
# Empty string comment should suppress existing comment
im.save(out, save_all=True, comment="")
with Image.open(out) as reread:
for frame in ImageSequence.Iterator(reread):
assert "comment" not in frame.info
def test_retain_comment_in_subsequent_frames(tmp_path):
# Test that a comment block at the beginning is kept
with Image.open("Tests/images/chi.gif") as im:
for frame in ImageSequence.Iterator(im):
assert frame.info["comment"] == b"Created with GIMP"
with Image.open("Tests/images/second_frame_comment.gif") as im:
assert "comment" not in im.info
# Test that a comment in the middle is read
im.seek(1)
assert im.info["comment"] == b"Comment in the second frame"
# Test that it is still present in a later frame
im.seek(2)
assert im.info["comment"] == b"Comment in the second frame"
# Test that rewinding removes the comment
im.seek(0)
assert "comment" not in im.info
# Test that a saved image keeps the comment
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/dispose_prev.gif") as im:
im.save(out, save_all=True, comment="Test")
with Image.open(out) as reread:
for frame in ImageSequence.Iterator(reread):
assert frame.info["comment"] == b"Test"
def test_version(tmp_path):
out = str(tmp_path / "temp.gif")
def assertVersionAfterSave(im, version):
def assert_version_after_save(im, version):
im.save(out)
with Image.open(out) as reread:
assert reread.info["version"] == version
# Test that GIF87a is used by default
im = Image.new("L", (100, 100), "#000")
assertVersionAfterSave(im, b"GIF87a")
assert_version_after_save(im, b"GIF87a")
# Test setting the version to 89a
im = Image.new("L", (100, 100), "#000")
im.info["version"] = b"89a"
assertVersionAfterSave(im, b"GIF89a")
assert_version_after_save(im, b"GIF89a")
# Test that adding a GIF89a feature changes the version
im.info["transparency"] = 1
assertVersionAfterSave(im, b"GIF89a")
assert_version_after_save(im, b"GIF89a")
# Test that a GIF87a image is also saved in that format
with Image.open("Tests/images/test.colors.gif") as im:
assertVersionAfterSave(im, b"GIF87a")
assert_version_after_save(im, b"GIF87a")
# Test that a GIF89a image is also saved in that format
im.info["version"] = b"GIF89a"
assertVersionAfterSave(im, b"GIF87a")
assert_version_after_save(im, b"GIF87a")
def test_append_images(tmp_path):
@ -838,10 +962,10 @@ def test_append_images(tmp_path):
assert reread.n_frames == 3
# Tests appending using a generator
def imGenerator(ims):
def im_generator(ims):
yield from ims
im.save(out, save_all=True, append_images=imGenerator(ims))
im.save(out, save_all=True, append_images=im_generator(ims))
with Image.open(out) as reread:
assert reread.n_frames == 3

View File

@ -4,15 +4,13 @@ import warnings
import pytest
from PIL import IcnsImagePlugin, Image, _binary, features
from PIL import IcnsImagePlugin, Image, _binary
from .helper import assert_image_equal, assert_image_similar_tofile
from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless_feature
# sample icon file
TEST_FILE = "Tests/images/pillow.icns"
ENABLE_JPEG2K = features.check_codec("jpg_2000")
def test_sanity():
# Loading this icon by default should result in the largest size
@ -111,14 +109,12 @@ def test_older_icon():
assert im2.size == (wr, hr)
@skip_unless_feature("jpg_2000")
def test_jp2_icon():
# This icon uses JPEG 2000 images instead of the PNG images.
# The advantage of doing this is that OS X 10.5 supports JPEG 2000
# but not PNG; some commercial software therefore does just this.
if not ENABLE_JPEG2K:
return
with Image.open("Tests/images/pillow3.icns") as im:
for w, h, r in im.info["sizes"]:
wr = w * r
@ -149,6 +145,7 @@ def test_not_an_icns_file():
IcnsImagePlugin.IcnsFile(fp)
@skip_unless_feature("jpg_2000")
def test_icns_decompression_bomb():
with Image.open(
"Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns"

View File

@ -754,7 +754,7 @@ class TestFileJpeg:
# Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated."
# http://www.exiv2.org/tags.html
# https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self):

View File

@ -298,6 +298,11 @@ def test_16bit_jp2_roundtrips():
assert_image_equal(im, jp2)
def test_issue_6194():
with Image.open("Tests/images/issue_6194.j2k") as im:
assert im.getpixel((5, 5)) == 31
def test_unbound_local():
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError):

View File

@ -497,8 +497,8 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, compression="tiff_adobe_deflate")
assert_image_equal_tofile(im, out)
def test_palette_save(self, tmp_path):
im = hopper("P")
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
def test_palette_save(self, im, tmp_path):
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True

View File

@ -48,6 +48,14 @@ def test_closed_file():
im.close()
def test_seek_after_close():
im = Image.open(test_files[0])
im.close()
with pytest.raises(ValueError):
im.seek(1)
def test_context_manager():
with warnings.catch_warnings():
with Image.open(test_files[0]) as im:
@ -116,6 +124,15 @@ def test_parallax():
assert exif.get_ifd(0x927C)[0xB211] == -3.125
def test_reload_exif_after_seek():
with Image.open("Tests/images/sugarshack.mpo") as im:
exif = im.getexif()
del exif[296]
im.seek(1)
assert 296 in exif
def test_mp():
for test_file in test_files:
with Image.open(test_file) as im:
@ -145,10 +162,10 @@ def test_mp_attribute():
for test_file in test_files:
with Image.open(test_file) as im:
mpinfo = im._getmp()
frameNumber = 0
frame_number = 0
for mpentry in mpinfo[0xB002]:
mpattr = mpentry["Attribute"]
if frameNumber:
if frame_number:
assert not mpattr["RepresentativeImageFlag"]
else:
assert mpattr["RepresentativeImageFlag"]
@ -157,7 +174,7 @@ def test_mp_attribute():
assert mpattr["ImageDataFormat"] == "JPEG"
assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
assert mpattr["Reserved"] == 0
frameNumber += 1
frame_number += 1
def test_seek():

View File

@ -131,10 +131,10 @@ def test_save_all(tmp_path):
assert os.path.getsize(outfile) > 0
# Test appending using a generator
def imGenerator(ims):
def im_generator(ims):
yield from ims
im.save(outfile, save_all=True, append_images=imGenerator(ims))
im.save(outfile, save_all=True, append_images=im_generator(ims))
assert os.path.isfile(outfile)
assert os.path.getsize(outfile) > 0
@ -253,9 +253,9 @@ def test_pdf_append(tmp_path):
check_pdf_pages_consistency(pdf)
# append two images
mode_CMYK = hopper("CMYK")
mode_P = hopper("P")
mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P])
mode_cmyk = hopper("CMYK")
mode_p = hopper("P")
mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p])
# open the PDF again, check pages and info again
with PdfParser.PdfParser(pdf_filename) as pdf:

View File

@ -643,6 +643,17 @@ class TestFilePng:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize("cid", (b"IHDR", b"pHYs", b"acTL", b"fcTL", b"fdAT"))
def test_truncated_chunks(self, cid):
fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png:
with pytest.raises(ValueError):
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = True
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_specify_bits(self, tmp_path):
im = hopper("P")

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest
from PIL import Image, UnidentifiedImageError
from PIL import Image, PpmImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -22,6 +22,21 @@ def test_sanity():
@pytest.mark.parametrize(
"data, mode, pixels",
(
(b"P2 3 1 4 0 2 4", "L", (0, 128, 255)),
(b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)),
# P3 with maxval < 255
(
b"P3 3 1 17 0 1 2 8 9 10 15 16 17",
"RGB",
((0, 15, 30), (120, 135, 150), (225, 240, 255)),
),
# P3 with maxval > 255
# Scale down to 255, since there is no RGB mode with more than 8-bit
(
b"P3 3 1 257 0 1 2 128 129 130 256 257 257",
"RGB",
((0, 1, 2), (127, 128, 129), (254, 255, 255)),
),
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
# P6 with maxval < 255
@ -35,7 +50,6 @@ def test_sanity():
),
),
# P6 with maxval > 255
# Scale down to 255, since there is no RGB mode with more than 8-bit
(
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
@ -85,14 +99,111 @@ def test_pnm(tmp_path):
assert_image_equal_tofile(im, f)
def test_magic(tmp_path):
@pytest.mark.parametrize(
"plain_path, raw_path",
(
(
"Tests/images/hopper_1bit_plain.pbm", # P1
"Tests/images/hopper_1bit.pbm", # P4
),
(
"Tests/images/hopper_8bit_plain.pgm", # P2
"Tests/images/hopper_8bit.pgm", # P5
),
(
"Tests/images/hopper_8bit_plain.ppm", # P3
"Tests/images/hopper_8bit.ppm", # P6
),
),
)
def test_plain(plain_path, raw_path):
with Image.open(plain_path) as im:
assert_image_equal_tofile(im, raw_path)
def test_16bit_plain_pgm():
# P2 with maxval 2 ** 16 - 1
with Image.open("Tests/images/hopper_16bit_plain.pgm") as im:
assert im.mode == "I"
assert im.size == (128, 128)
assert im.get_format_mimetype() == "image/x-portable-graymap"
# P5 with maxval 2 ** 16 - 1
assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
@pytest.mark.parametrize(
"header, data, comment_count",
(
(b"P1\n2 2", b"1010", 10**6),
(b"P2\n3 1\n4", b"0 2 4", 1),
(b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6),
),
)
def test_plain_data_with_comment(tmp_path, header, data, comment_count):
path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm")
comment = b"# comment" * comment_count
with open(path1, "wb") as f1, open(path2, "wb") as f2:
f1.write(header + b"\n\n" + data)
f2.write(header + b"\n" + comment + b"\n" + data + comment)
with Image.open(path1) as im:
assert_image_equal_tofile(im, path2)
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
def test_plain_truncated_data(tmp_path, data):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"PyInvalid")
f.write(data)
with pytest.raises(UnidentifiedImageError):
with Image.open(path):
pass
with Image.open(path) as im:
with pytest.raises(ValueError):
im.load()
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
def test_plain_invalid_data(tmp_path, data):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
with Image.open(path) as im:
with pytest.raises(ValueError):
im.load()
@pytest.mark.parametrize(
"data",
(
b"P3\n128 128\n255\n012345678910", # half token too long
b"P3\n128 128\n255\n012345678910 0", # token too long
),
)
def test_plain_ppm_token_too_long(tmp_path, data):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(data)
with Image.open(path) as im:
with pytest.raises(ValueError):
im.load()
def test_plain_ppm_value_too_large(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P3\n128 128\n255\n256")
with Image.open(path) as im:
with pytest.raises(ValueError):
im.load()
def test_magic():
with pytest.raises(SyntaxError):
PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid"))
def test_header_with_comments(tmp_path):
@ -114,7 +225,7 @@ def test_non_integer_token(tmp_path):
pass
def test_token_too_long(tmp_path):
def test_header_token_too_long(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
@ -123,7 +234,7 @@ def test_token_too_long(tmp_path):
with Image.open(path):
pass
assert str(e.value) == "Token too long in file header: b'01234567890'"
assert str(e.value) == "Token too long in file header: 01234567890"
def test_truncated_file(tmp_path):
@ -145,6 +256,19 @@ def test_truncated_file(tmp_path):
im.load()
@pytest.mark.parametrize("maxval", (0, 65536))
def test_invalid_maxval(maxval, tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "w") as f:
f.write("P6\n3 1 " + str(maxval))
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the
# internal open_ppm function didn't check for sanity but it

View File

@ -101,6 +101,10 @@ def test_cross_scan_line():
with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
with Image.open("Tests/images/cross_scan_line_truncated.tga") as im:
with pytest.raises(OSError):
im.load()
def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga"

View File

@ -70,6 +70,15 @@ class TestFileTiff:
im.load()
im.close()
def test_seek_after_close(self):
im = Image.open("Tests/images/multipage.tiff")
im.close()
with pytest.raises(ValueError):
im.n_frames
with pytest.raises(ValueError):
im.seek(1)
def test_context_manager(self):
with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im:
@ -92,17 +101,33 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
@pytest.mark.parametrize(
"file_name,mode,size,offset",
"file_name,mode,size,tile",
[
("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160),
("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8),
(
"tiff_wrong_bits_per_sample.tiff",
"RGBA",
(52, 53),
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
),
(
"tiff_wrong_bits_per_sample_2.tiff",
"RGB",
(16, 16),
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
),
(
"tiff_wrong_bits_per_sample_3.tiff",
"RGBA",
(512, 256),
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
),
],
)
def test_wrong_bits_per_sample(self, file_name, mode, size, offset):
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
assert im.size == size
assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))]
assert im.tile == tile
im.load()
def test_set_legacy_api(self):
@ -151,14 +176,14 @@ class TestFileTiff:
assert im.info["dpi"] == (71.0, 71.0)
@pytest.mark.parametrize(
"resolutionUnit, dpi",
"resolution_unit, dpi",
[(None, 72.8), (2, 72.8), (3, 184.912)],
)
def test_load_float_dpi(self, resolutionUnit, dpi):
def test_load_float_dpi(self, resolution_unit, dpi):
with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif"
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im:
assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
assert im.info["dpi"] == (dpi, dpi)
def test_save_float_dpi(self, tmp_path):
@ -472,6 +497,26 @@ class TestFileTiff:
exif = im.getexif()
check_exif(exif)
def test_modify_exif(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
exif[256] = 100
im.save(outfile, exif=exif)
with Image.open(outfile) as im:
exif = im.getexif()
assert exif[256] == 100
def test_reload_exif_after_seek(self):
with Image.open("Tests/images/multipage.tiff") as im:
exif = im.getexif()
del exif[256]
im.seek(1)
assert 256 in exif
def test_exif_frames(self):
# Test that EXIF data can change across frames
with Image.open("Tests/images/g4-multi.tiff") as im:
@ -655,11 +700,11 @@ class TestFileTiff:
assert reread.n_frames == 3
# Test appending using a generator
def imGenerator(ims):
def im_generator(ims):
yield from ims
mp = BytesIO()
im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims))
im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
@ -690,6 +735,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert reloaded.info["icc_profile"] == icc_profile
def test_save_bmp_compression(self, tmp_path):
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["compression"] == 0
outfile = str(tmp_path / "temp.tif")
im.save(outfile)
def test_discard_icc_profile(self, tmp_path):
outfile = str(tmp_path / "temp.tif")

View File

@ -28,26 +28,26 @@ def test_rt_metadata(tmp_path):
# For text items, we still have to decode('ascii','replace') because
# the tiff file format can't take 8 bit bytes in that field.
basetextdata = "This is some arbitrary metadata for a text field"
bindata = basetextdata.encode("ascii") + b" \xff"
textdata = basetextdata + " " + chr(255)
reloaded_textdata = basetextdata + " ?"
floatdata = 12.345
doubledata = 67.89
base_text_data = "This is some arbitrary metadata for a text field"
bin_data = base_text_data.encode("ascii") + b" \xff"
text_data = base_text_data + " " + chr(255)
reloaded_text_data = base_text_data + " ?"
float_data = 12.345
double_data = 67.89
info = TiffImagePlugin.ImageFileDirectory()
ImageJMetaData = TAG_IDS["ImageJMetaData"]
ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
ImageDescription = TAG_IDS["ImageDescription"]
info[ImageJMetaDataByteCounts] = len(bindata)
info[ImageJMetaData] = bindata
info[TAG_IDS["RollAngle"]] = floatdata
info[ImageJMetaDataByteCounts] = len(bin_data)
info[ImageJMetaData] = bin_data
info[TAG_IDS["RollAngle"]] = float_data
info.tagtype[TAG_IDS["RollAngle"]] = 11
info[TAG_IDS["YawAngle"]] = doubledata
info[TAG_IDS["YawAngle"]] = double_data
info.tagtype[TAG_IDS["YawAngle"]] = 12
info[ImageDescription] = textdata
info[ImageDescription] = text_data
f = str(tmp_path / "temp.tif")
@ -55,28 +55,28 @@ def test_rt_metadata(tmp_path):
with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),)
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag[ImageJMetaData] == bindata
assert loaded.tag_v2[ImageJMetaData] == bindata
assert loaded.tag[ImageJMetaData] == bin_data
assert loaded.tag_v2[ImageJMetaData] == bin_data
assert loaded.tag[ImageDescription] == (reloaded_textdata,)
assert loaded.tag_v2[ImageDescription] == reloaded_textdata
assert loaded.tag[ImageDescription] == (reloaded_text_data,)
assert loaded.tag_v2[ImageDescription] == reloaded_text_data
loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
assert round(abs(loaded_float - floatdata), 5) == 0
assert round(abs(loaded_float - float_data), 5) == 0
loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
assert round(abs(loaded_double - doubledata), 7) == 0
assert round(abs(loaded_double - double_data), 7) == 0
# check with 2 element ImageJMetaDataByteCounts, issue #2006
info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8)
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
img.save(f, tiffinfo=info)
with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata():
@ -356,7 +356,7 @@ def test_empty_values():
assert 33432 in info
def test_PhotoshopInfo(tmp_path):
def test_photoshop_info(tmp_path):
with Image.open("Tests/images/issue_2278.tif") as im:
assert len(im.tag_v2[34377]) == 70
assert isinstance(im.tag_v2[34377], bytes)

View File

@ -191,6 +191,17 @@ class TestFileWebp:
Image.open(blob).load()
Image.open(blob).load()
@pytest.mark.parametrize(
"background",
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
)
@skip_unless_feature("webp_anim")
def test_invalid_background(self, background, tmp_path):
temp_file = str(tmp_path / "temp.webp")
im = hopper()
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):
# Save L mode GIF with background

View File

@ -90,14 +90,14 @@ def test_write_animation_RGB(tmp_path):
check(temp_file1)
# Tests appending using a generator
def imGenerator(ims):
def im_generator(ims):
yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp")
frame1.copy().save(
temp_file2,
save_all=True,
append_images=imGenerator([frame2]),
append_images=im_generator([frame2]),
lossless=True,
)
check(temp_file2)

View File

@ -7,7 +7,7 @@ import warnings
import pytest
from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError
from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError, features
from .helper import (
assert_image_equal,
@ -159,6 +159,8 @@ class TestImage:
assert im.size == (128, 128)
for ext in (".jpg", ".jp2"):
if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext))
if os.path.exists(temp_file):
os.remove(temp_file)
@ -168,7 +170,7 @@ class TestImage:
temp_file = str(tmp_path / "temp.jpg")
class FP:
def write(a, b):
def write(self, b):
pass
fp = FP()
@ -661,11 +663,34 @@ class TestImage:
with Image.open("Tests/images/hopper.gif") as im:
assert_image_equal(im, im.remap_palette(list(range(256))))
# Test identity transform with an RGBA palette
im = Image.new("P", (256, 1))
for x in range(256):
im.putpixel((x, 0), x)
im.putpalette(list(range(256)) * 4, "RGBA")
im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped)
assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode
with hopper() as im:
with pytest.raises(ValueError):
im.remap_palette(None)
def test_remap_palette_transparency(self):
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
im.info["transparency"] = 0
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
# Test unused transparency
im.info["transparency"] = 2
im_remapped = im.remap_palette([1, 0])
assert "transparency" not in im_remapped.info
def test__new(self):
im = hopper("RGB")
im_p = hopper("P")
@ -883,6 +908,35 @@ class TestImage:
im = Image.new("RGB", size)
assert im.tobytes() == b""
def test_apply_transparency(self):
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1))
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
# Test that no transformation is applied without transparency
im.apply_transparency()
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
# Test that a transparency index is applied
im.info["transparency"] = 0
im.apply_transparency()
assert "transparency" not in im.info
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1}
# Test that existing transparency is kept
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
im.info["transparency"] = 0
im.apply_transparency()
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.colors[(27, 35, 6)] == 24
im.apply_transparency()
assert im.palette.colors[(27, 35, 6, 214)] == 24
def test_categories_deprecation(self):
with pytest.warns(DeprecationWarning):
assert hopper().category == 0

View File

@ -80,3 +80,15 @@ def test_fromarray():
with pytest.raises(TypeError):
wrapped = Wrapper(test("L"), {"shape": (100, 128)})
Image.fromarray(wrapped)
def test_fromarray_palette():
# Arrange
i = im.convert("L")
a = numpy.array(i)
# Act
out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match
assert len(out.palette.colors) == len(out.im.getpalette()) / 3

View File

@ -27,15 +27,15 @@ def test_sanity():
"HSV",
)
for mode in modes:
im = hopper(mode)
for mode in modes:
convert(im, mode)
for input_mode in modes:
im = hopper(input_mode)
for output_mode in modes:
convert(im, output_mode)
# Check 0
im = Image.new(mode, (0, 0))
for mode in modes:
convert(im, mode)
im = Image.new(input_mode, (0, 0))
for output_mode in modes:
convert(im, output_mode)
def test_default():
@ -222,6 +222,20 @@ def test_p_la():
assert_image_similar(alpha, comparable, 5)
def test_p2pa_alpha():
with Image.open("Tests/images/tiny.png") as im:
assert im.mode == "P"
im_pa = im.convert("PA")
assert im_pa.mode == "PA"
im_a = im_pa.getchannel("A")
for x in range(4):
alpha = 255 if x > 1 else 0
for y in range(4):
assert im_a.getpixel((x, y)) == alpha
def test_matrix_illegal_conversion():
# Arrange
im = hopper("CMYK")

View File

@ -6,8 +6,8 @@ from .helper import hopper
def test_copy():
croppedCoordinates = (10, 10, 20, 20)
croppedSize = (10, 10)
cropped_coordinates = (10, 10, 20, 20)
cropped_size = (10, 10)
for mode in "1", "P", "L", "RGB", "I", "F":
# Internal copy method
im = hopper(mode)
@ -23,15 +23,15 @@ def test_copy():
# Internal copy method on a cropped image
im = hopper(mode)
out = im.crop(croppedCoordinates).copy()
out = im.crop(cropped_coordinates).copy()
assert out.mode == im.mode
assert out.size == croppedSize
assert out.size == cropped_size
# Python's copy method on a cropped image
im = hopper(mode)
out = copy.copy(im.crop(croppedCoordinates))
out = copy.copy(im.crop(cropped_coordinates))
assert out.mode == im.mode
assert out.size == croppedSize
assert out.size == cropped_size
def test_copy_zero():

View File

@ -99,10 +99,10 @@ def test_rankfilter_properties():
def test_builtinfilter_p():
builtinFilter = ImageFilter.BuiltinFilter()
builtin_filter = ImageFilter.BuiltinFilter()
with pytest.raises(ValueError):
builtinFilter.filter(hopper("P"))
builtin_filter.filter(hopper("P"))
def test_kernel_not_enough_coefficients():

View File

@ -1,6 +1,12 @@
import warnings
import pytest
from PIL import Image, ImageQt
from PIL import Image
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
from .helper import assert_image_equal, hopper

View File

@ -1,6 +1,6 @@
from PIL import Image
from .helper import assert_image_equal, cached_property
from .helper import CachedProperty, assert_image_equal
class TestImagingPaste:
@ -34,7 +34,7 @@ class TestImagingPaste:
im.paste(im2, mask)
self.assert_9points_image(im, expected)
@cached_property
@CachedProperty
def mask_1(self):
mask = Image.new("1", (self.size, self.size))
px = mask.load()
@ -43,11 +43,11 @@ class TestImagingPaste:
px[y, x] = (x + y) % 2
return mask
@cached_property
@CachedProperty
def mask_L(self):
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@cached_property
@CachedProperty
def gradient_L(self):
gradient = Image.new("L", (self.size, self.size))
px = gradient.load()
@ -56,7 +56,7 @@ class TestImagingPaste:
px[y, x] = (x + y) % 255
return gradient
@cached_property
@CachedProperty
def gradient_RGB(self):
return Image.merge(
"RGB",
@ -67,7 +67,7 @@ class TestImagingPaste:
],
)
@cached_property
@CachedProperty
def gradient_LA(self):
return Image.merge(
"LA",
@ -77,7 +77,7 @@ class TestImagingPaste:
],
)
@cached_property
@CachedProperty
def gradient_RGBA(self):
return Image.merge(
"RGBA",
@ -89,7 +89,7 @@ class TestImagingPaste:
],
)
@cached_property
@CachedProperty
def gradient_RGBa(self):
return Image.merge(
"RGBa",

View File

@ -1,5 +1,7 @@
import pytest
from PIL import Image
from .helper import assert_image_equal, hopper
@ -10,17 +12,31 @@ def test_sanity():
im.point(list(range(256)))
im.point(list(range(256)) * 3)
im.point(lambda x: x)
im.point(lambda x: x * 1.2)
im = im.convert("I")
with pytest.raises(ValueError):
im.point(list(range(256)))
im.point(lambda x: x * 1)
im.point(lambda x: x + 1)
im.point(lambda x: x * 1 + 1)
with pytest.raises(TypeError):
im.point(lambda x: x - 1)
with pytest.raises(TypeError):
im.point(lambda x: x * 1 + 1)
im.point(lambda x: 0.1 + 0.2 * x)
im.point(lambda x: -x)
im.point(lambda x: x - 0.5)
im.point(lambda x: 1 - x / 2)
im.point(lambda x: (2 + x) / 3)
im.point(lambda x: 0.5)
im.point(lambda x: x / 1)
im.point(lambda x: x + x)
with pytest.raises(TypeError):
im.point(lambda x: x * x)
with pytest.raises(TypeError):
im.point(lambda x: x / x)
with pytest.raises(TypeError):
im.point(lambda x: 1 / x)
with pytest.raises(TypeError):
im.point(lambda x: x // 2)
def test_16bit_lut():
@ -46,3 +62,8 @@ def test_f_mode():
im = hopper("F")
with pytest.raises(ValueError):
im.point(None)
def test_coerce_e_deprecation():
with pytest.warns(DeprecationWarning):
assert Image.coerce_e(2).data == 2

View File

@ -458,7 +458,7 @@ class TestCoreResampleBox:
def split_range(size, tiles):
scale = size / tiles
for i in range(tiles):
yield (int(round(scale * i)), int(round(scale * (i + 1))))
yield int(round(scale * i)), int(round(scale * (i + 1)))
tiled = Image.new(im.mode, dst_size)
scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1])

View File

@ -12,6 +12,7 @@ from .helper import (
assert_image_equal_tofile,
assert_image_similar,
hopper,
skip_unless_feature,
)
@ -264,6 +265,14 @@ class TestImageResize:
with pytest.raises(ValueError):
im.resize((10, 10), "unknown")
@skip_unless_feature("libtiff")
def test_load_first(self):
# load() may change the size of the image
# Test that resize() is calling it before getting the size
with Image.open("Tests/images/g4_orientation_5.tif") as im:
im = im.resize((64, 64))
assert im.size == (64, 64)
def test_default_filter(self):
for mode in "L", "RGB", "I", "F":
im = hopper(mode)

View File

@ -7,6 +7,7 @@ from .helper import (
assert_image_similar,
fromstring,
hopper,
skip_unless_feature,
tostring,
)
@ -88,6 +89,7 @@ def test_no_resize():
assert im.size == (64, 64)
@skip_unless_feature("libtiff")
def test_load_first():
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations

View File

@ -655,6 +655,20 @@ def test_polygon_1px_high():
assert_image_equal_tofile(im, expected)
def test_polygon_1px_high_translucent():
# Test drawing a translucent 1px high polygon
# Arrange
im = Image.new("RGB", (4, 3))
draw = ImageDraw.Draw(im, "RGBA")
expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png"
# Act
draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127))
# Assert
assert_image_equal_tofile(im, expected)
def test_polygon_translucent():
# Arrange
im = Image.new("RGB", (W, H))
@ -1452,3 +1466,11 @@ def test_discontiguous_corners_polygon():
)
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
assert_image_similar_tofile(img, expected, 1)
def test_polygon():
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
expected = "Tests/images/imagedraw_outline_polygon_RGB.png"
assert_image_similar_tofile(im, expected, 1)

View File

@ -2,7 +2,15 @@ from io import BytesIO
import pytest
from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features
from PIL import (
BmpImagePlugin,
EpsImagePlugin,
Image,
ImageFile,
UnidentifiedImageError,
_binary,
features,
)
from .helper import (
assert_image,
@ -35,9 +43,9 @@ class TestImageFile:
parser = ImageFile.Parser()
parser.feed(data)
imOut = parser.close()
im_out = parser.close()
return im, imOut
return im, im_out
assert_image_equal(*roundtrip("BMP"))
im1, im2 = roundtrip("GIF")
@ -377,3 +385,7 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)
def test_zero_height(self):
with pytest.raises(UnidentifiedImageError):
Image.open("Tests/images/zero_height.j2k")

View File

@ -65,9 +65,12 @@ class TestImageFont:
return font_bytes
def test_font_with_filelike(self):
ImageFont.truetype(
ttf = ImageFont.truetype(
self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
)
ttf_copy = ttf.font_variant()
assert ttf_copy.font_bytes == ttf.font_bytes
self._render(self._font_as_bytes())
# Usage note: making two fonts from the same buffer fails.
# shared_bytes = self._font_as_bytes()
@ -977,6 +980,14 @@ class TestImageFont:
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
def test_fill_deprecation(self):
font = self.get_font()
with pytest.warns(DeprecationWarning):
font.getmask2("Hello world", fill=Image.core.fill)
with pytest.warns(DeprecationWarning):
with pytest.raises(TypeError):
font.getmask2("Hello world", fill=None)
@skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont):

View File

@ -48,12 +48,8 @@ def img_string_normalize(im):
return img_to_string(string_to_img(im))
def assert_img_equal(A, B):
assert img_to_string(A) == img_to_string(B)
def assert_img_equal_img_string(A, Bstring):
assert img_to_string(A) == img_string_normalize(Bstring)
def assert_img_equal_img_string(a, b_string):
assert img_to_string(a) == img_string_normalize(b_string)
def test_str_to_img():

View File

@ -174,7 +174,7 @@ def test_overflow_segfault():
# through to the sequence. Seeing this on 32-bit Windows.
with pytest.raises((TypeError, MemoryError)):
# post patch, this fails with a memory error
x = evil()
x = Evil()
# This fails due to the invalid malloc above,
# and segfaults
@ -182,7 +182,7 @@ def test_overflow_segfault():
x[i] = b"0" * 16
class evil:
class Evil:
def __init__(self):
self.corrupt = Image.core.path(0x4000000000000000)

View File

@ -2,9 +2,12 @@ import warnings
import pytest
from .helper import assert_image_similar, hopper
with warnings.catch_warnings() as w:
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
from .helper import assert_image_similar, hopper
pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"

View File

@ -65,21 +65,21 @@ def test_libtiff():
def test_consecutive():
with Image.open("Tests/images/multipage.tiff") as im:
firstFrame = None
first_frame = None
for frame in ImageSequence.Iterator(im):
if firstFrame is None:
firstFrame = frame.copy()
if first_frame is None:
first_frame = frame.copy()
for frame in ImageSequence.Iterator(im):
assert_image_equal(frame, firstFrame)
assert_image_equal(frame, first_frame)
break
def test_palette_mmap():
# Using mmap in ImageFile can require to reload the palette.
with Image.open("Tests/images/multipage-mmap.tiff") as im:
color1 = im.getpalette()[0:3]
color1 = im.getpalette()[:3]
im.seek(0)
color2 = im.getpalette()[0:3]
color2 = im.getpalette()[:3]
assert color1 == color2

View File

@ -26,6 +26,8 @@ def setup_module():
# setup tk
tk.Frame()
# root = tk.Tk()
except RuntimeError as v:
pytest.skip(f"RuntimeError: {v}")
except tk.TclError as v:
pytest.skip(f"TCL Error: {v}")

View File

@ -26,51 +26,51 @@ def test_basic(tmp_path):
def basic(mode):
imIn = original.convert(mode)
verify(imIn)
im_in = original.convert(mode)
verify(im_in)
w, h = imIn.size
w, h = im_in.size
imOut = imIn.copy()
verify(imOut) # copy
im_out = im_in.copy()
verify(im_out) # copy
imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(imOut) # transform
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(im_out) # transform
filename = str(tmp_path / "temp.im")
imIn.save(filename)
im_in.save(filename)
with Image.open(filename) as imOut:
with Image.open(filename) as im_out:
verify(imIn)
verify(imOut)
verify(im_in)
verify(im_out)
imOut = imIn.crop((0, 0, w, h))
verify(imOut)
im_out = im_in.crop((0, 0, w, h))
verify(im_out)
imOut = Image.new(mode, (w, h), None)
imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0))
imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0))
im_out = Image.new(mode, (w, h), None)
im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
verify(imIn)
verify(imOut)
verify(im_in)
verify(im_out)
imIn = Image.new(mode, (1, 1), 1)
assert imIn.getpixel((0, 0)) == 1
im_in = Image.new(mode, (1, 1), 1)
assert im_in.getpixel((0, 0)) == 1
imIn.putpixel((0, 0), 2)
assert imIn.getpixel((0, 0)) == 2
im_in.putpixel((0, 0), 2)
assert im_in.getpixel((0, 0)) == 2
if mode == "L":
maximum = 255
else:
maximum = 32767
imIn = Image.new(mode, (1, 1), 256)
assert imIn.getpixel((0, 0)) == min(256, maximum)
im_in = Image.new(mode, (1, 1), 256)
assert im_in.getpixel((0, 0)) == min(256, maximum)
imIn.putpixel((0, 0), 512)
assert imIn.getpixel((0, 0)) == min(512, maximum)
im_in.putpixel((0, 0), 512)
assert im_in.getpixel((0, 0)) == min(512, maximum)
basic("L")

View File

@ -1,5 +1,9 @@
import warnings
import pytest
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper

View File

@ -1,5 +1,9 @@
import warnings
import pytest
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper

View File

@ -8,7 +8,7 @@ def test_is_path():
fp = "filename.ext"
# Act
it_is = _util.isPath(fp)
it_is = _util.is_path(fp)
# Assert
assert it_is
@ -21,7 +21,7 @@ def test_path_obj_is_path():
test_path = Path("filename.ext")
# Act
it_is = _util.isPath(test_path)
it_is = _util.is_path(test_path)
# Assert
assert it_is
@ -33,7 +33,7 @@ def test_is_not_path(tmp_path):
pass
# Act
it_is_not = _util.isPath(fp)
it_is_not = _util.is_path(fp)
# Assert
assert not it_is_not
@ -44,7 +44,7 @@ def test_is_directory():
directory = "Tests"
# Act
it_is = _util.isDirectory(directory)
it_is = _util.is_directory(directory)
# Assert
assert it_is
@ -55,7 +55,7 @@ def test_is_not_directory():
text = "abc"
# Act
it_is_not = _util.isDirectory(text)
it_is_not = _util.is_directory(text)
# Assert
assert not it_is_not
@ -65,7 +65,7 @@ def test_deferred_error():
# Arrange
# Act
thing = _util.deferred_error(ValueError("Some error text"))
thing = _util.DeferredError(ValueError("Some error text"))
# Assert
with pytest.raises(ValueError):

View File

@ -1,7 +1,7 @@
#!/bin/bash
# install openjpeg
archive=openjpeg-2.4.0
archive=openjpeg-2.5.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -68,7 +68,7 @@ release = PIL.__version__
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:

View File

@ -97,8 +97,8 @@ Deprecated Use instead
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX``
``Image.BILINEAR`` ``Image.Resampling.BILNEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
``Image.LINEAR`` ``Image.Resampling.BILINEAR``
``Image.HAMMING`` ``Image.Resampling.HAMMING``
``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
``Image.CUBIC`` ``Image.Resampling.BICUBIC``
@ -142,6 +142,14 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead.
FreeTypeFont.getmask2 fill parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
deprecated and will be removed in Pillow 10 (2023-07-01).
PhotoImage.paste box parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -149,6 +157,27 @@ PhotoImage.paste box parameter
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
PyQt5 and PySide2
~~~~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
Image.coerce_e
~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
This undocumented method has been deprecated and will be removed in Pillow 10
(2023-07-01).
Removed features
----------------

View File

@ -17,9 +17,9 @@ When an image is opened from a file, only that instance of the image is consider
have the format. Copies of the image will contain data loaded from the file, but not
the file itself, meaning that it can no longer be considered to be in the original
format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
internally creates a copy of the image, the ``fp`` (file pointer), along with any
methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format`
attribute will be ``None``.
internally creates a copy of the image, then any methods or attributes specific to the
format will no longer be present. The ``fp`` (file pointer) attribute will no longer be
present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
Fully supported formats
-----------------------
@ -101,8 +101,8 @@ GIF
^^^
Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
writes run-length encoded files in GIF87a by default, unless GIF89a features
are used or GIF89a is already in use.
writes files in GIF87a by default, unless GIF89a features are used or GIF89a is
already in use. Files are written with LZW encoding.
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
images. Seeking to later frames in a ``P`` image will change the image to
@ -156,7 +156,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
it will loop forever.
**comment**
May not be present. A comment about the image.
May not be present. A comment about the image. This is the last comment found
before the current frame's image.
**extension**
May not be present. Contains application specific information.
@ -245,17 +246,14 @@ Reading local images
The GIF loader creates an image memory the same size as the GIF files *logical
screen size*, and pastes the actual pixel data (the *local image*) into this
image. If you only want the actual pixel rectangle, you can manipulate the
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.ImageFile.ImageFile.tile`
attributes before loading the file::
image. If you only want the actual pixel rectangle, you can crop the image::
im = Image.open(...)
if im.tile[0][0] == "gif":
# only read the first "local image" from this GIF file
tag, (x0, y0, x1, y1), offset, extra = im.tile[0]
im.size = (x1 - x0, y1 - y0)
im.tile = [(tag, (0, 0) + im.size, offset, extra)]
box = im.tile[0][1]
im = im.crop(box)
ICNS
^^^^
@ -1235,6 +1233,11 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
SUN
^^^
Pillow identifies and reads Sun raster files.
WAL
^^^
@ -1249,13 +1252,13 @@ this format.
By default, a Quake2 standard palette is attached to the texture. To override
the palette, use the putpalette method.
WMF
^^^
WMF, EMF
^^^^^^^^
Pillow can identify WMF files.
Pillow can identify WMF and EMF files.
On Windows, it can read WMF files. By default, it will load the image at 72
dpi. To load it at another resolution:
On Windows, it can read WMF and EMF files. By default, it will load the image
at 72 dpi. To load it at another resolution:
.. code-block:: python
@ -1265,7 +1268,8 @@ dpi. To load it at another resolution:
im.load(dpi=144)
To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler.
.. code-block:: python

View File

@ -504,6 +504,17 @@ image header. In addition, seek will also be used when the image data is read
tar file, you can use the :py:class:`~PIL.ContainerIO` or
:py:class:`~PIL.TarIO` modules to access it.
Reading from URL
^^^^^^^^^^^^^^^^
::
from PIL import Image
from urllib.request import urlopen
url = "https://python-pillow.org/images/pillow-logo.png"
img = Image.open(urlopen(url))
Reading from a tar archive
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -29,6 +29,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
:alt: GitHub Actions build status (Test MinGW)
.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
:alt: GitHub Actions build status (Test Cygwin)
.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build
:target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows)

View File

@ -162,7 +162,7 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.3**
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.4**
* **libfreetype** provides type related services
@ -181,7 +181,8 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1** and **2.4.0**.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
**2.4.0** and **2.5.0**.
* Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie.
@ -463,22 +464,29 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Fedora 35 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 36 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
| +----------------------------+---------------------+
| | 3.8 | arm64v8, ppc64le, |
| | | s390x |
| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, |
| | | s390x, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.7 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 |
| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 |
| | PyPy3 | |
| +----------------------------+---------------------+
| | 3.9/MinGW | x86, x86-64 |
| | 3.9 (MinGW) | x86, x86-64 |
| +----------------------------+---------------------+
| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+
@ -496,13 +504,13 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |arm |
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |arm |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+--------------+
| | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |x86-64 |
| +---------------------------+------------------+--------------+
| | 3.6 | 8.4.0 |x86-64 |
| | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |x86-64 |
| +---------------------------+------------------+ |
| | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
| +---------------------------+------------------+ |
@ -563,6 +571,8 @@ These platforms have been reported to work at the versions mentioned.
+----------------------------------+---------------------------+------------------+--------------+
| Windows 10 | 3.7 | 7.1.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+
| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
+----------------------------------+---------------------------+------------------+--------------+
| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |

View File

@ -123,6 +123,7 @@ methods. Unless otherwise stated, all methods return a new instance of the
.. automethod:: PIL.Image.Image.alpha_composite
.. automethod:: PIL.Image.Image.apply_transparency
.. automethod:: PIL.Image.Image.convert
The following example converts an RGB image (linearly calibrated according to

View File

@ -7,6 +7,14 @@
The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5
or PySide2 QImage objects from PIL images.
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
.. versionadded:: 1.1.6
.. py:class:: ImageQt(image)

View File

@ -25,3 +25,8 @@ The :py:class:`~PIL.ImageSequence.Iterator` class
.. autoclass:: PIL.ImageSequence.Iterator
:members:
Functions
---------
.. autofunction:: PIL.ImageSequence.all_frames

View File

@ -10,6 +10,10 @@ metadata tag numbers, names, and type information.
.. method:: lookup(tag)
:param tag: Integer tag number
:param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in
.. versionadded:: 8.3.0
:returns: Taginfo namedtuple, From the :py:data:`~PIL.TiffTags.TAGS_V2` info if possible,
otherwise just populating the value and name from :py:data:`~PIL.TiffTags.TAGS`.
If the tag is not recognized, "unknown" is returned for the name
@ -42,6 +46,16 @@ metadata tag numbers, names, and type information.
.. versionadded:: 3.0.0
.. py:data:: PIL.TiffTags.TAGS_V2_GROUPS
:type: dict
:py:data:`~PIL.TiffTags.TAGS_V2` is one dimensional and
doesn't account for the fact that tags actually exist in
`different groups <https://exiftool.org/TagNames/EXIF.html>`_.
This dictionary is used when the tag in question is part of a group.
.. versionadded:: 8.3.0
.. py:data:: PIL.TiffTags.TAGS
:type: dict

View File

@ -9,6 +9,14 @@ Internal Modules
:undoc-members:
:show-inheritance:
:mod:`~PIL._deprecate` Module
-----------------------------
.. automodule:: PIL._deprecate
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL._tkinter_finder` Module
----------------------------------

View File

@ -174,7 +174,7 @@ Previously, if a BMP file was too large, an ``OSError`` would be raised. Now,
Dark theme for docs
^^^^^^^^^^^^^^^^^^^
The https://pillow.readthedocs.io documentation will use a dark theme if the the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.

View File

@ -149,6 +149,9 @@ Switched to libjpeg-turbo in macOS and Linux wheels
The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to
libjpeg-turbo. It is a fork of libjpeg, popular for its speed.
Because different JPEG decoders load images differently, JPEG pixels may be
altered slightly with this change.
Added support for pickling TrueType fonts
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -76,8 +76,8 @@ Deprecated Use instead
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX``
``Image.BILINEAR`` ``Image.Resampling.BILNEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
``Image.LINEAR`` ``Image.Resampling.BILINEAR``
``Image.HAMMING`` ``Image.Resampling.HAMMING``
``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
``Image.CUBIC`` ``Image.Resampling.BICUBIC``

View File

@ -0,0 +1,16 @@
9.1.1
-----
Security
========
This release addresses several security problems.
:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines,
Pillow reads the information past the end of the first line without deducting that
from the length of the remaining file data. This vulnerability was introduced in Pillow
9.1.0, and can cause a heap buffer overflow.
Opening an image with a zero or negative height has been found to bypass a
decompression bomb check. This will now raise a :py:exc:`SyntaxError` instead, in turn
raising a ``PIL.UnidentifiedImageError``.

View File

@ -0,0 +1,82 @@
9.2.0
-----
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
PyQt5 and PySide2
^^^^^^^^^^^^^^^^^
.. deprecated:: 9.2.0
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
FreeTypeFont.getmask2 fill parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 9.2.0
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2`
has been deprecated and will be removed in Pillow 10 (2023-07-01).
PhotoImage.paste box parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 9.2.0
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
Image.coerce_e
^^^^^^^^^^^^^^
.. deprecated:: 9.2.0
This undocumented method has been deprecated and will be removed in Pillow 10
(2023-07-01).
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
Image.apply_transparency
^^^^^^^^^^^^^^^^^^^^^^^^
Added :py:meth:`~PIL.Image.Image.apply_transparency`, a method to take a P mode image
with "transparency" in ``im.info``, and apply the transparency to the palette instead.
The image's palette mode will become "RGBA", and "transparency" will be removed from
``im.info``.
Security
========
TODO
^^^^
TODO
Other Changes
=============
TODO
^^^^
TODO

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