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 # gather the coverage data
python3 -m pip install codecov python3 -m pip install codecov
if [[ $MATRIX_DOCKER ]]; then if [[ $MATRIX_DOCKER ]]; then
coverage xml --ignore-errors python3 -m coverage xml --ignore-errors
else else
coverage xml python3 -m coverage xml
fi fi

View File

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

View File

@ -13,13 +13,17 @@ aptget_update()
return 1 return 1
fi fi
} }
aptget_update || aptget_update retry || aptget_update retry if [[ $(uname) != CYGWIN* ]]; then
aptget_update || aptget_update retry || aptget_update retry
fi
set -e set -e
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev cmake meson imagemagick libharfbuzz-dev libfribidi-dev
fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
@ -32,24 +36,28 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install test-image-results python3 -m pip install test-image-results
python3 -m pip install numpy
# PyQt5 doesn't support PyPy3 if [[ $(uname) != CYGWIN* ]]; then
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then # TODO Remove condition when NumPy supports 3.11
# arm64, ppc64le, s390x CPUs: if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools # PyQt6 doesn't support PyPy3
python3 -m pip install pyqt5 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
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
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
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 fi
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && ./install_raqm.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

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

View File

@ -4,7 +4,7 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
## Bug fixes, feature additions, etc. ## 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. - Fork the Pillow repository.
- Create a branch from `main`. - 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=Docker Test Successful
- status-success=Windows Test Successful - status-success=Windows Test Successful
- status-success=MinGW Test Successful - status-success=MinGW Test Successful
- status-success=Cygwin Test Successful
- status-success=continuous-integration/appveyor/pr - status-success=continuous-integration/appveyor/pr
actions: actions:
merge: merge:

View File

@ -15,7 +15,8 @@ python3 -m pip install pyroma
python3 -m pip install test-image-results python3 -m pip install test-image-results
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg 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 # extra test images
pushd depends && ./install_extra_test_images.sh && popd 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: matrix:
docker: [ docker: [
# Run slower jobs first to give them a headstart and reduce waiting time # Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-20.04-focal-arm64v8, ubuntu-22.04-jammy-arm64v8,
ubuntu-20.04-focal-ppc64le, ubuntu-22.04-jammy-ppc64le,
ubuntu-20.04-focal-s390x, ubuntu-22.04-jammy-s390x,
# Then run the remainder # Then run the remainder
alpine, alpine,
amazon-2-amd64, amazon-2-amd64,
@ -24,17 +24,19 @@ jobs:
debian-10-buster-x86, debian-10-buster-x86,
debian-11-bullseye-x86, debian-11-bullseye-x86,
fedora-35-amd64, fedora-35-amd64,
fedora-36-amd64,
gentoo, gentoo,
ubuntu-18.04-bionic-amd64, ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
] ]
dockerTag: [main] dockerTag: [main]
include: include:
- docker: "ubuntu-20.04-focal-arm64v8" - docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64" qemu-arch: "aarch64"
- docker: "ubuntu-20.04-focal-ppc64le" - docker: "ubuntu-22.04-jammy-ppc64le"
qemu-arch: "ppc64le" qemu-arch: "ppc64le"
- docker: "ubuntu-20.04-focal-s390x" - docker: "ubuntu-22.04-jammy-s390x"
qemu-arch: "s390x" qemu-arch: "s390x"
name: ${{ matrix.docker }} name: ${{ matrix.docker }}

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ repos:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.1.13 rev: v1.2.0
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
@ -37,10 +37,15 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0 rev: v4.2.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-yaml - id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6
hooks:
- id: sphinx-lint
ci: 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: sdist:
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
python3 -m build --sdist 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 .PHONY: test
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 <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
alt="GitHub Actions build status (Test MinGW)" alt="GitHub Actions build status (Test MinGW)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a> 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 <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)" alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] 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.: * [ ] 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] 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.: * [ ] 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] 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) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)

View File

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

View File

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

View File

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

View File

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

View File

@ -46,6 +46,15 @@ def test_closed_file():
im.close() 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(): def test_context_manager():
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest import pytest
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -46,6 +46,19 @@ def test_closed_file():
im.close() 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(): def test_context_manager():
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
@ -341,8 +354,15 @@ def test_seek_rewind():
assert_image_equal(im, expected) assert_image_equal(im, expected)
def test_n_frames(): @pytest.mark.parametrize(
for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: "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 # Test is_animated before n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert im.is_animated == (n_frames != 1) 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) 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: with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0 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") 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(): def test_no_transparency_in_second_frame():
with Image.open("Tests/images/iss634.gif") as img: 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 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): def test_duration(tmp_path):
duration = 1000 duration = 1000
@ -691,6 +736,23 @@ def test_multiple_duration(tmp_path):
pass 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): def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000] 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 = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops) im.save(out, loop=number_of_loops)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["loop"] == number_of_loops 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): def test_background(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -777,6 +846,9 @@ def test_comment(tmp_path):
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["comment"] == im.info["comment"].encode() 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): def test_comment_over_255(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -787,43 +859,95 @@ def test_comment_over_255(tmp_path):
im.info["comment"] = comment im.info["comment"] = comment
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["comment"] == comment assert reread.info["comment"] == comment
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
def test_zero_comment_subblocks(): def test_zero_comment_subblocks():
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
assert_image_equal_tofile(im, TEST_GIF) 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): def test_version(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
def assertVersionAfterSave(im, version): def assert_version_after_save(im, version):
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["version"] == version assert reread.info["version"] == version
# Test that GIF87a is used by default # Test that GIF87a is used by default
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
assertVersionAfterSave(im, b"GIF87a") assert_version_after_save(im, b"GIF87a")
# Test setting the version to 89a # Test setting the version to 89a
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.info["version"] = b"89a" 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 # Test that adding a GIF89a feature changes the version
im.info["transparency"] = 1 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 # Test that a GIF87a image is also saved in that format
with Image.open("Tests/images/test.colors.gif") as im: 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 # Test that a GIF89a image is also saved in that format
im.info["version"] = b"GIF89a" im.info["version"] = b"GIF89a"
assertVersionAfterSave(im, b"GIF87a") assert_version_after_save(im, b"GIF87a")
def test_append_images(tmp_path): def test_append_images(tmp_path):
@ -838,10 +962,10 @@ def test_append_images(tmp_path):
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending using a generator # Tests appending using a generator
def imGenerator(ims): def im_generator(ims):
yield from 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: with Image.open(out) as reread:
assert reread.n_frames == 3 assert reread.n_frames == 3

View File

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

View File

@ -754,7 +754,7 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated." # "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) assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self): def test_invalid_exif(self):

View File

@ -298,6 +298,11 @@ def test_16bit_jp2_roundtrips():
assert_image_equal(im, jp2) 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(): def test_unbound_local():
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception. # prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError): with pytest.raises(OSError):

View File

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

View File

@ -48,6 +48,14 @@ def test_closed_file():
im.close() 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(): def test_context_manager():
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(test_files[0]) as im: with Image.open(test_files[0]) as im:
@ -116,6 +124,15 @@ def test_parallax():
assert exif.get_ifd(0x927C)[0xB211] == -3.125 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(): def test_mp():
for test_file in test_files: for test_file in test_files:
with Image.open(test_file) as im: with Image.open(test_file) as im:
@ -145,10 +162,10 @@ def test_mp_attribute():
for test_file in test_files: for test_file in test_files:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
frameNumber = 0 frame_number = 0
for mpentry in mpinfo[0xB002]: for mpentry in mpinfo[0xB002]:
mpattr = mpentry["Attribute"] mpattr = mpentry["Attribute"]
if frameNumber: if frame_number:
assert not mpattr["RepresentativeImageFlag"] assert not mpattr["RepresentativeImageFlag"]
else: else:
assert mpattr["RepresentativeImageFlag"] assert mpattr["RepresentativeImageFlag"]
@ -157,7 +174,7 @@ def test_mp_attribute():
assert mpattr["ImageDataFormat"] == "JPEG" assert mpattr["ImageDataFormat"] == "JPEG"
assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
assert mpattr["Reserved"] == 0 assert mpattr["Reserved"] == 0
frameNumber += 1 frame_number += 1
def test_seek(): def test_seek():

View File

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

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest import pytest
from PIL import Image, UnidentifiedImageError from PIL import Image, PpmImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar, hopper from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -22,6 +22,21 @@ def test_sanity():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"data, mode, pixels", "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 4 \x00\x02\x04", "L", (0, 128, 255)),
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
# P6 with maxval < 255 # P6 with maxval < 255
@ -35,7 +50,6 @@ def test_sanity():
), ),
), ),
# P6 with maxval > 255 # 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"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", 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) 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") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"PyInvalid") f.write(data)
with pytest.raises(UnidentifiedImageError): with Image.open(path) as im:
with Image.open(path): with pytest.raises(ValueError):
pass 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): def test_header_with_comments(tmp_path):
@ -114,7 +225,7 @@ def test_non_integer_token(tmp_path):
pass pass
def test_token_too_long(tmp_path): def test_header_token_too_long(tmp_path):
path = str(tmp_path / "temp.ppm") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6\n 01234567890") f.write(b"P6\n 01234567890")
@ -123,7 +234,7 @@ def test_token_too_long(tmp_path):
with Image.open(path): with Image.open(path):
pass 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): def test_truncated_file(tmp_path):
@ -145,6 +256,19 @@ def test_truncated_file(tmp_path):
im.load() 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(): def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the # Storage.c accepted negative values for xsize, ysize. the
# internal open_ppm function didn't check for sanity but it # 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: with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") 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): def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga" test_file = "Tests/images/tga_id_field.tga"

View File

@ -70,6 +70,15 @@ class TestFileTiff:
im.load() im.load()
im.close() 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): def test_context_manager(self):
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
@ -92,17 +101,33 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif") assert_image_equal_tofile(im, "Tests/images/hopper.tif")
@pytest.mark.parametrize( @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: with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode assert im.mode == mode
assert im.size == size assert im.size == size
assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))] assert im.tile == tile
im.load() im.load()
def test_set_legacy_api(self): def test_set_legacy_api(self):
@ -151,14 +176,14 @@ class TestFileTiff:
assert im.info["dpi"] == (71.0, 71.0) assert im.info["dpi"] == (71.0, 71.0)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"resolutionUnit, dpi", "resolution_unit, dpi",
[(None, 72.8), (2, 72.8), (3, 184.912)], [(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( with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif" "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im: ) 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) assert im.info["dpi"] == (dpi, dpi)
def test_save_float_dpi(self, tmp_path): def test_save_float_dpi(self, tmp_path):
@ -472,6 +497,26 @@ class TestFileTiff:
exif = im.getexif() exif = im.getexif()
check_exif(exif) 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): def test_exif_frames(self):
# Test that EXIF data can change across frames # Test that EXIF data can change across frames
with Image.open("Tests/images/g4-multi.tiff") as im: with Image.open("Tests/images/g4-multi.tiff") as im:
@ -655,11 +700,11 @@ class TestFileTiff:
assert reread.n_frames == 3 assert reread.n_frames == 3
# Test appending using a generator # Test appending using a generator
def imGenerator(ims): def im_generator(ims):
yield from ims yield from ims
mp = BytesIO() 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) mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread: with Image.open(mp) as reread:
@ -690,6 +735,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert reloaded.info["icc_profile"] == icc_profile 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): def test_discard_icc_profile(self, tmp_path):
outfile = str(tmp_path / "temp.tif") 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 # For text items, we still have to decode('ascii','replace') because
# the tiff file format can't take 8 bit bytes in that field. # the tiff file format can't take 8 bit bytes in that field.
basetextdata = "This is some arbitrary metadata for a text field" base_text_data = "This is some arbitrary metadata for a text field"
bindata = basetextdata.encode("ascii") + b" \xff" bin_data = base_text_data.encode("ascii") + b" \xff"
textdata = basetextdata + " " + chr(255) text_data = base_text_data + " " + chr(255)
reloaded_textdata = basetextdata + " ?" reloaded_text_data = base_text_data + " ?"
floatdata = 12.345 float_data = 12.345
doubledata = 67.89 double_data = 67.89
info = TiffImagePlugin.ImageFileDirectory() info = TiffImagePlugin.ImageFileDirectory()
ImageJMetaData = TAG_IDS["ImageJMetaData"] ImageJMetaData = TAG_IDS["ImageJMetaData"]
ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
ImageDescription = TAG_IDS["ImageDescription"] ImageDescription = TAG_IDS["ImageDescription"]
info[ImageJMetaDataByteCounts] = len(bindata) info[ImageJMetaDataByteCounts] = len(bin_data)
info[ImageJMetaData] = bindata info[ImageJMetaData] = bin_data
info[TAG_IDS["RollAngle"]] = floatdata info[TAG_IDS["RollAngle"]] = float_data
info.tagtype[TAG_IDS["RollAngle"]] = 11 info.tagtype[TAG_IDS["RollAngle"]] = 11
info[TAG_IDS["YawAngle"]] = doubledata info[TAG_IDS["YawAngle"]] = double_data
info.tagtype[TAG_IDS["YawAngle"]] = 12 info.tagtype[TAG_IDS["YawAngle"]] = 12
info[ImageDescription] = textdata info[ImageDescription] = text_data
f = str(tmp_path / "temp.tif") f = str(tmp_path / "temp.tif")
@ -55,28 +55,28 @@ def test_rt_metadata(tmp_path):
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag[ImageJMetaData] == bindata assert loaded.tag[ImageJMetaData] == bin_data
assert loaded.tag_v2[ImageJMetaData] == bindata assert loaded.tag_v2[ImageJMetaData] == bin_data
assert loaded.tag[ImageDescription] == (reloaded_textdata,) assert loaded.tag[ImageDescription] == (reloaded_text_data,)
assert loaded.tag_v2[ImageDescription] == reloaded_textdata assert loaded.tag_v2[ImageDescription] == reloaded_text_data
loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] 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] 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 # 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) img.save(f, tiffinfo=info)
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata(): def test_read_metadata():
@ -356,7 +356,7 @@ def test_empty_values():
assert 33432 in info 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: with Image.open("Tests/images/issue_2278.tif") as im:
assert len(im.tag_v2[34377]) == 70 assert len(im.tag_v2[34377]) == 70
assert isinstance(im.tag_v2[34377], bytes) assert isinstance(im.tag_v2[34377], bytes)

View File

@ -191,6 +191,17 @@ class TestFileWebp:
Image.open(blob).load() Image.open(blob).load()
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") @skip_unless_feature("webp_anim")
def test_background_from_gif(self, tmp_path): def test_background_from_gif(self, tmp_path):
# Save L mode GIF with background # Save L mode GIF with background

View File

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

View File

@ -7,7 +7,7 @@ import warnings
import pytest import pytest
from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError, features
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -159,6 +159,8 @@ class TestImage:
assert im.size == (128, 128) assert im.size == (128, 128)
for ext in (".jpg", ".jp2"): 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)) temp_file = str(tmp_path / ("temp." + ext))
if os.path.exists(temp_file): if os.path.exists(temp_file):
os.remove(temp_file) os.remove(temp_file)
@ -168,7 +170,7 @@ class TestImage:
temp_file = str(tmp_path / "temp.jpg") temp_file = str(tmp_path / "temp.jpg")
class FP: class FP:
def write(a, b): def write(self, b):
pass pass
fp = FP() fp = FP()
@ -661,11 +663,34 @@ class TestImage:
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
assert_image_equal(im, im.remap_palette(list(range(256)))) 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 # Test illegal image mode
with hopper() as im: with hopper() as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.remap_palette(None) 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): def test__new(self):
im = hopper("RGB") im = hopper("RGB")
im_p = hopper("P") im_p = hopper("P")
@ -883,6 +908,35 @@ class TestImage:
im = Image.new("RGB", size) im = Image.new("RGB", size)
assert im.tobytes() == b"" 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): def test_categories_deprecation(self):
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert hopper().category == 0 assert hopper().category == 0

View File

@ -80,3 +80,15 @@ def test_fromarray():
with pytest.raises(TypeError): with pytest.raises(TypeError):
wrapped = Wrapper(test("L"), {"shape": (100, 128)}) wrapped = Wrapper(test("L"), {"shape": (100, 128)})
Image.fromarray(wrapped) 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", "HSV",
) )
for mode in modes: for input_mode in modes:
im = hopper(mode) im = hopper(input_mode)
for mode in modes: for output_mode in modes:
convert(im, mode) convert(im, output_mode)
# Check 0 # Check 0
im = Image.new(mode, (0, 0)) im = Image.new(input_mode, (0, 0))
for mode in modes: for output_mode in modes:
convert(im, mode) convert(im, output_mode)
def test_default(): def test_default():
@ -222,6 +222,20 @@ def test_p_la():
assert_image_similar(alpha, comparable, 5) 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(): def test_matrix_illegal_conversion():
# Arrange # Arrange
im = hopper("CMYK") im = hopper("CMYK")

View File

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

View File

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

View File

@ -1,6 +1,12 @@
import warnings
import pytest 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 from .helper import assert_image_equal, hopper

View File

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

View File

@ -1,5 +1,7 @@
import pytest import pytest
from PIL import Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
@ -10,17 +12,31 @@ def test_sanity():
im.point(list(range(256))) im.point(list(range(256)))
im.point(list(range(256)) * 3) im.point(list(range(256)) * 3)
im.point(lambda x: x) im.point(lambda x: x)
im.point(lambda x: x * 1.2)
im = im.convert("I") im = im.convert("I")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.point(list(range(256))) im.point(list(range(256)))
im.point(lambda x: x * 1) im.point(lambda x: x * 1)
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) 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 / 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(): def test_16bit_lut():
@ -46,3 +62,8 @@ def test_f_mode():
im = hopper("F") im = hopper("F")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.point(None) 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): def split_range(size, tiles):
scale = size / tiles scale = size / tiles
for i in range(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) tiled = Image.new(im.mode, dst_size)
scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1]) 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_equal_tofile,
assert_image_similar, assert_image_similar,
hopper, hopper,
skip_unless_feature,
) )
@ -264,6 +265,14 @@ class TestImageResize:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.resize((10, 10), "unknown") 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): def test_default_filter(self):
for mode in "L", "RGB", "I", "F": for mode in "L", "RGB", "I", "F":
im = hopper(mode) im = hopper(mode)

View File

@ -7,6 +7,7 @@ from .helper import (
assert_image_similar, assert_image_similar,
fromstring, fromstring,
hopper, hopper,
skip_unless_feature,
tostring, tostring,
) )
@ -88,6 +89,7 @@ def test_no_resize():
assert im.size == (64, 64) assert im.size == (64, 64)
@skip_unless_feature("libtiff")
def test_load_first(): def test_load_first():
# load() may change the size of the image # load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations # 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) 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(): def test_polygon_translucent():
# Arrange # Arrange
im = Image.new("RGB", (W, H)) 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") expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
assert_image_similar_tofile(img, expected, 1) 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 import pytest
from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features from PIL import (
BmpImagePlugin,
EpsImagePlugin,
Image,
ImageFile,
UnidentifiedImageError,
_binary,
features,
)
from .helper import ( from .helper import (
assert_image, assert_image,
@ -35,9 +43,9 @@ class TestImageFile:
parser = ImageFile.Parser() parser = ImageFile.Parser()
parser.feed(data) parser.feed(data)
imOut = parser.close() im_out = parser.close()
return im, imOut return im, im_out
assert_image_equal(*roundtrip("BMP")) assert_image_equal(*roundtrip("BMP"))
im1, im2 = roundtrip("GIF") im1, im2 = roundtrip("GIF")
@ -377,3 +385,7 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None) 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 return font_bytes
def test_font_with_filelike(self): def test_font_with_filelike(self):
ImageFont.truetype( ttf = ImageFont.truetype(
self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE 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()) self._render(self._font_as_bytes())
# Usage note: making two fonts from the same buffer fails. # Usage note: making two fonts from the same buffer fails.
# shared_bytes = self._font_as_bytes() # shared_bytes = self._font_as_bytes()
@ -977,6 +980,14 @@ class TestImageFont:
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) 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") @skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont): class TestImageFont_RaqmLayout(TestImageFont):

View File

@ -48,12 +48,8 @@ def img_string_normalize(im):
return img_to_string(string_to_img(im)) return img_to_string(string_to_img(im))
def assert_img_equal(A, B): def assert_img_equal_img_string(a, b_string):
assert img_to_string(A) == img_to_string(B) assert img_to_string(a) == img_string_normalize(b_string)
def assert_img_equal_img_string(A, Bstring):
assert img_to_string(A) == img_string_normalize(Bstring)
def test_str_to_img(): 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. # through to the sequence. Seeing this on 32-bit Windows.
with pytest.raises((TypeError, MemoryError)): with pytest.raises((TypeError, MemoryError)):
# post patch, this fails with a memory error # post patch, this fails with a memory error
x = evil() x = Evil()
# This fails due to the invalid malloc above, # This fails due to the invalid malloc above,
# and segfaults # and segfaults
@ -182,7 +182,7 @@ def test_overflow_segfault():
x[i] = b"0" * 16 x[i] = b"0" * 16
class evil: class Evil:
def __init__(self): def __init__(self):
self.corrupt = Image.core.path(0x4000000000000000) self.corrupt = Image.core.path(0x4000000000000000)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install openjpeg # 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 ./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. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # 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 # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:

View File

@ -97,8 +97,8 @@ Deprecated Use instead
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` ``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` ``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX`` ``Image.BOX`` ``Image.Resampling.BOX``
``Image.BILINEAR`` ``Image.Resampling.BILNEAR`` ``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR`` ``Image.LINEAR`` ``Image.Resampling.BILINEAR``
``Image.HAMMING`` ``Image.Resampling.HAMMING`` ``Image.HAMMING`` ``Image.Resampling.HAMMING``
``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` ``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
``Image.CUBIC`` ``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 Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead. :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 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). 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 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 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 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 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 internally creates a copy of the image, then any methods or attributes specific to the
methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format` format will no longer be present. The ``fp`` (file pointer) attribute will no longer be
attribute will be ``None``. present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
Fully supported formats Fully supported formats
----------------------- -----------------------
@ -101,8 +101,8 @@ GIF
^^^ ^^^
Pillow reads GIF87a and GIF89a versions of the GIF file format. The library 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 writes files in GIF87a by default, unless GIF89a features are used or GIF89a is
are used or GIF89a is already in use. already in use. Files are written with LZW encoding.
GIF files are initially read as grayscale (``L``) or palette mode (``P``) 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 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. it will loop forever.
**comment** **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** **extension**
May not be present. Contains application specific information. 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 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 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 image. If you only want the actual pixel rectangle, you can crop the image::
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.ImageFile.ImageFile.tile`
attributes before loading the file::
im = Image.open(...) im = Image.open(...)
if im.tile[0][0] == "gif": if im.tile[0][0] == "gif":
# only read the first "local image" from this GIF file # only read the first "local image" from this GIF file
tag, (x0, y0, x1, y1), offset, extra = im.tile[0] box = im.tile[0][1]
im.size = (x1 - x0, y1 - y0) im = im.crop(box)
im.tile = [(tag, (0, 0) + im.size, offset, extra)]
ICNS ICNS
^^^^ ^^^^
@ -1235,6 +1233,11 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
SUN
^^^
Pillow identifies and reads Sun raster files.
WAL WAL
^^^ ^^^
@ -1249,13 +1252,13 @@ this format.
By default, a Quake2 standard palette is attached to the texture. To override By default, a Quake2 standard palette is attached to the texture. To override
the palette, use the putpalette method. 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 On Windows, it can read WMF and EMF files. By default, it will load the image
dpi. To load it at another resolution: at 72 dpi. To load it at another resolution:
.. code-block:: python .. code-block:: python
@ -1265,7 +1268,8 @@ dpi. To load it at another resolution:
im.load(dpi=144) im.load(dpi=144)
To add other read or write support, use 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 .. 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 tar file, you can use the :py:class:`~PIL.ContainerIO` or
:py:class:`~PIL.TarIO` modules to access it. :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 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 :target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
:alt: GitHub Actions build status (Test MinGW) :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 .. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build
:target: https://ci.appveyor.com/project/python-pillow/Pillow :target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows) :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 * **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 * **libfreetype** provides type related services
@ -181,7 +181,8 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality. * **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 * Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie. with Debian Jessie.
@ -463,22 +464,29 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 35 | 3.10 | x86-64 | | Fedora 35 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 36 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | 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 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 | | Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
| +----------------------------+---------------------+ | | PyPy3 | |
| | 3.8 | arm64v8, ppc64le, | +----------------------------------+----------------------------+---------------------+
| | | s390x | | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, |
| | | s390x, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.7 | 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 | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | 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 | | 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.7, 3.8, 3.9, 3.10 | 9.1.1 |x86-64 |
| +---------------------------+------------------+--------------+ | +---------------------------+------------------+ |
| | 3.6 | 8.4.0 |x86-64 | | | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | | 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 | 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.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 | | 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.alpha_composite
.. automethod:: PIL.Image.Image.apply_transparency
.. automethod:: PIL.Image.Image.convert .. automethod:: PIL.Image.Image.convert
The following example converts an RGB image (linearly calibrated according to 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 The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5
or PySide2 QImage objects from PIL images. 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 .. versionadded:: 1.1.6
.. py:class:: ImageQt(image) .. py:class:: ImageQt(image)

View File

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

View File

@ -10,6 +10,10 @@ metadata tag numbers, names, and type information.
.. method:: lookup(tag) .. method:: lookup(tag)
:param tag: Integer tag number :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, :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`. 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 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 .. 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 .. py:data:: PIL.TiffTags.TAGS
:type: dict :type: dict

View File

@ -9,6 +9,14 @@ Internal Modules
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL._deprecate` Module
-----------------------------
.. automodule:: PIL._deprecate
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL._tkinter_finder` Module :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 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 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. 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 Added support for pickling TrueType fonts
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -76,8 +76,8 @@ Deprecated Use instead
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` ``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` ``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX`` ``Image.BOX`` ``Image.Resampling.BOX``
``Image.BILINEAR`` ``Image.Resampling.BILNEAR`` ``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR`` ``Image.LINEAR`` ``Image.Resampling.BILINEAR``
``Image.HAMMING`` ``Image.Resampling.HAMMING`` ``Image.HAMMING`` ``Image.Resampling.HAMMING``
``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` ``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
``Image.CUBIC`` ``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