Merge branch 'main' into plainPPM

This commit is contained in:
Andrew Murray 2022-06-12 16:11:17 +10:00
commit 5051a29a4e
196 changed files with 4593 additions and 4747 deletions

View File

@ -25,8 +25,8 @@ install:
- mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs9550w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH%
- ..\pillow-depends\gs9561w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
@ -43,7 +43,7 @@ build_script:
test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov'
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

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

View File

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

View File

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

View File

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

1
.github/mergify.yml vendored
View File

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

View File

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

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

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

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

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

View File

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

View File

@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
architecture: ["x86", "x64"]
include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
@ -41,10 +41,10 @@ jobs:
cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information
run: python .github/workflows/system-info.py
run: python3 .github/workflows/system-info.py
- name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
- name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
- name: Install dependencies
id: install
@ -52,8 +52,8 @@ jobs:
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
winbuild\depends\gs9550w32.exe /S
echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH
winbuild\depends\gs9561w32.exe /S
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\

View File

@ -15,6 +15,7 @@ jobs:
python-version: [
"pypy-3.8",
"pypy-3.7",
"3.11-dev",
"3.10",
"3.9",
"3.8",
@ -59,6 +60,8 @@ jobs:
if: startsWith(matrix.os, 'macOS')
run: |
.github/workflows/macos-install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Build
run: |
@ -93,7 +96,7 @@ jobs:
- name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: |
python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
make doccheck
- name: After success

View File

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

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: fc0be6eb1e2a96091e6f64009ee5e9081bf8b6c6 # frozen: 22.1.0
rev: 22.3.0
hooks:
- id: black
args: ["--target-version", "py37"]
@ -9,38 +9,43 @@ repos:
types: []
- repo: https://github.com/PyCQA/isort
rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/asottile/yesqa
rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0
rev: v1.3.0
hooks:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: ca52c4245639abd55c970e6bbbca95cab3de22d8 # frozen: v1.1.13
rev: v1.2.0
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8
rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1
rev: 4.0.1
hooks:
- id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
rev: v1.9.0
hooks:
- id: python-check-blanket-noqa
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0
rev: v4.2.0
hooks:
- id: check-merge-conflict
- id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6
hooks:
- id: sphinx-lint
ci:
autoupdate_schedule: quarterly
autoupdate_schedule: monthly

File diff suppressed because it is too large Load Diff

View File

@ -77,7 +77,7 @@ release-test:
-rm dist/*.egg
-rmdir dist
python3 -m pytest -qq
python3 -m check-manifest
python3 -m check_manifest
python3 -m pyroma .
$(MAKE) readme
@ -85,6 +85,8 @@ release-test:
sdist:
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
python3 -m build --sdist
python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
python3 -m twine check --strict dist/*
.PHONY: test
test:

View File

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

View File

@ -24,13 +24,12 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create and check source distribution:
```bash
make sdist
twine check dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
```bash
twine check dist/*
twine upload dist/Pillow-5.2.0*
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
@ -61,13 +60,12 @@ Released as needed for security, installation or critical bug fixes.
* [ ] Create and check source distribution:
```bash
make sdist
twine check dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
```bash
twine check dist/*
twine upload dist/Pillow-5.2.1*
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.1*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Create and check source distribution:
```bash
make sdist
twine check dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)

View File

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

View File

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

BIN
Tests/images/16bit.r.tif Normal file

Binary file not shown.

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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

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

BIN
Tests/images/no_palette.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 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

@ -40,6 +40,7 @@ def test_questionable():
"rgb32fakealpha.bmp",
"rgb24largepal.bmp",
"pal8os2sp.bmp",
"pal8rletrns.bmp",
"rgb32bf-xbgr.bmp",
]
for f in get_files("q"):

View File

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

View File

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

View File

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

91
Tests/test_deprecate.py Normal file
View File

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

View File

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

View File

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

View File

@ -4,7 +4,12 @@ import pytest
from PIL import BmpImagePlugin, Image
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar_tofile,
hopper,
)
def test_sanity(tmp_path):
@ -125,6 +130,42 @@ def test_rgba_bitfields():
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
def test_rle8():
with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
# This test image has been manually hexedited
# to have rows with too much data
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
# Signal end of bitmap before the image is finished
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
data = fp.read(1063) + b"\x01"
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(ValueError):
im.load()
@pytest.mark.parametrize(
"file_name,length",
(
# EOF immediately after the header
("Tests/images/hopper_rle8.bmp", 1078),
# EOF during delta
("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
# EOF when reading data in absolute mode
("Tests/images/bmp/g/pal8rle.bmp", 1064),
),
)
def test_rle8_eof(file_name, length):
with open(file_name, "rb") as fp:
data = fp.read(length)
with Image.open(io.BytesIO(data)) as im:
with pytest.raises(ValueError):
im.load()
def test_offset():
# This image has been hexedited
# to exclude the palette size from the pixel data offset

View File

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

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features
from .helper import (
assert_image_equal,
@ -46,6 +46,19 @@ def test_closed_file():
im.close()
def test_seek_after_close():
im = Image.open("Tests/images/iss634.gif")
im.load()
im.close()
with pytest.raises(ValueError):
im.is_animated
with pytest.raises(ValueError):
im.n_frames
with pytest.raises(ValueError):
im.seek(1)
def test_context_manager():
with warnings.catch_warnings():
with Image.open(TEST_GIF) as im:
@ -59,6 +72,51 @@ def test_invalid_file():
GifImagePlugin.GifImageFile(invalid_file)
def test_l_mode_transparency():
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
assert im.mode == "L"
assert im.load()[0, 0] == 128
assert im.info["transparency"] == 255
im.seek(1)
assert im.mode == "L"
assert im.load()[0, 0] == 128
def test_strategy():
with Image.open("Tests/images/chi.gif") as im:
expected_zero = im.convert("RGB")
im.seek(1)
expected_one = im.convert("RGB")
try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGB"
assert_image_equal(im, expected_zero)
GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
)
# Stay in P mode with only a global palette
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_one)
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "P"
im.seek(1)
assert im.mode == "RGB"
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_optimize():
def test_grayscale(optimize):
im = Image.new("L", (1, 1), 0)
@ -296,16 +354,23 @@ def test_seek_rewind():
assert_image_equal(im, expected)
def test_n_frames():
for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]:
# Test is_animated before n_frames
with Image.open(path) as im:
assert im.is_animated == (n_frames != 1)
@pytest.mark.parametrize(
"path, n_frames",
(
(TEST_GIF, 1),
("Tests/images/comment_after_last_frame.gif", 2),
("Tests/images/iss634.gif", 42),
),
)
def test_n_frames(path, n_frames):
# Test is_animated before n_frames
with Image.open(path) as im:
assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
def test_no_change():
@ -383,18 +448,38 @@ def test_dispose_background_transparency():
assert px[35, 30][3] == 0
def test_transparent_dispose():
expected_colors = [
(2, 1, 2),
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
]
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
@pytest.mark.parametrize(
"loading_strategy, expected_colors",
(
(
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
(
(2, 1, 2),
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
),
),
(
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
(
(2, 1, 2),
(0, 1, 0),
(2, 1, 2),
),
),
),
)
def test_transparent_dispose(loading_strategy, expected_colors):
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x]
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_dispose_previous():
@ -554,7 +639,8 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0)
def test_transparency_in_second_frame():
def test_transparency_in_second_frame(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0
@ -564,6 +650,14 @@ def test_transparency_in_second_frame():
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
im.save(out, save_all=True)
with Image.open(out) as reread:
reread.seek(reread.tell() + 1)
assert_image_equal_tofile(
reread, "Tests/images/different_transparency_merged.png"
)
def test_no_transparency_in_second_frame():
with Image.open("Tests/images/iss634.gif") as img:
@ -575,6 +669,22 @@ def test_no_transparency_in_second_frame():
assert img.histogram()[255] == 0
def test_remapped_transparency(tmp_path):
out = str(tmp_path / "temp.gif")
im = Image.new("P", (1, 2))
im2 = im.copy()
# Add transparency at a higher index
# so that it will be optimized to a lower index
im.putpixel((0, 1), 5)
im.info["transparency"] = 5
im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reloaded:
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
def test_duration(tmp_path):
duration = 1000
@ -626,6 +736,23 @@ def test_multiple_duration(tmp_path):
pass
def test_roundtrip_info_duration(tmp_path):
duration_list = [100, 500, 500]
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/transparent_dispose.gif") as im:
assert [
frame.info["duration"] for frame in ImageSequence.Iterator(im)
] == duration_list
im.save(out, save_all=True)
with Image.open(out) as reloaded:
assert [
frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
] == duration_list
def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000]
@ -677,9 +804,16 @@ def test_number_of_loops(tmp_path):
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops)
with Image.open(out) as reread:
assert reread.info["loop"] == number_of_loops
# Check that even if a subsequent GIF frame has the number of loops specified,
# only the value from the first frame is used
with Image.open("Tests/images/duplicate_number_of_loops.gif") as im:
assert im.info["loop"] == 2
im.seek(1)
assert im.info["loop"] == 2
def test_background(tmp_path):
out = str(tmp_path / "temp.gif")
@ -712,6 +846,9 @@ def test_comment(tmp_path):
with Image.open(out) as reread:
assert reread.info["comment"] == im.info["comment"].encode()
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
def test_comment_over_255(tmp_path):
out = str(tmp_path / "temp.gif")
@ -722,43 +859,95 @@ def test_comment_over_255(tmp_path):
im.info["comment"] = comment
im.save(out)
with Image.open(out) as reread:
assert reread.info["comment"] == comment
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
def test_zero_comment_subblocks():
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
assert_image_equal_tofile(im, TEST_GIF)
def test_read_multiple_comment_blocks():
with Image.open("Tests/images/multiple_comments.gif") as im:
# Multiple comment blocks in a frame are separated not concatenated
assert im.info["comment"] == b"Test comment 1\nTest comment 2"
def test_empty_string_comment(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/chi.gif") as im:
assert "comment" in im.info
# Empty string comment should suppress existing comment
im.save(out, save_all=True, comment="")
with Image.open(out) as reread:
for frame in ImageSequence.Iterator(reread):
assert "comment" not in frame.info
def test_retain_comment_in_subsequent_frames(tmp_path):
# Test that a comment block at the beginning is kept
with Image.open("Tests/images/chi.gif") as im:
for frame in ImageSequence.Iterator(im):
assert frame.info["comment"] == b"Created with GIMP"
with Image.open("Tests/images/second_frame_comment.gif") as im:
assert "comment" not in im.info
# Test that a comment in the middle is read
im.seek(1)
assert im.info["comment"] == b"Comment in the second frame"
# Test that it is still present in a later frame
im.seek(2)
assert im.info["comment"] == b"Comment in the second frame"
# Test that rewinding removes the comment
im.seek(0)
assert "comment" not in im.info
# Test that a saved image keeps the comment
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/dispose_prev.gif") as im:
im.save(out, save_all=True, comment="Test")
with Image.open(out) as reread:
for frame in ImageSequence.Iterator(reread):
assert frame.info["comment"] == b"Test"
def test_version(tmp_path):
out = str(tmp_path / "temp.gif")
def assertVersionAfterSave(im, version):
def assert_version_after_save(im, version):
im.save(out)
with Image.open(out) as reread:
assert reread.info["version"] == version
# Test that GIF87a is used by default
im = Image.new("L", (100, 100), "#000")
assertVersionAfterSave(im, b"GIF87a")
assert_version_after_save(im, b"GIF87a")
# Test setting the version to 89a
im = Image.new("L", (100, 100), "#000")
im.info["version"] = b"89a"
assertVersionAfterSave(im, b"GIF89a")
assert_version_after_save(im, b"GIF89a")
# Test that adding a GIF89a feature changes the version
im.info["transparency"] = 1
assertVersionAfterSave(im, b"GIF89a")
assert_version_after_save(im, b"GIF89a")
# Test that a GIF87a image is also saved in that format
with Image.open("Tests/images/test.colors.gif") as im:
assertVersionAfterSave(im, b"GIF87a")
assert_version_after_save(im, b"GIF87a")
# Test that a GIF89a image is also saved in that format
im.info["version"] = b"GIF89a"
assertVersionAfterSave(im, b"GIF87a")
assert_version_after_save(im, b"GIF87a")
def test_append_images(tmp_path):
@ -773,10 +962,10 @@ def test_append_images(tmp_path):
assert reread.n_frames == 3
# Tests appending using a generator
def imGenerator(ims):
def im_generator(ims):
yield from ims
im.save(out, save_all=True, append_images=imGenerator(ims))
im.save(out, save_all=True, append_images=im_generator(ims))
with Image.open(out) as reread:
assert reread.n_frames == 3
@ -831,6 +1020,17 @@ def test_rgb_transparency(tmp_path):
assert "transparency" not in reloaded.info
def test_rgba_transparency(tmp_path):
out = str(tmp_path / "temp.gif")
im = hopper("P")
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
with Image.open(out) as reloaded:
reloaded.seek(1)
assert_image_equal(hopper("P").convert("RGB"), reloaded)
def test_bbox(tmp_path):
out = str(tmp_path / "temp.gif")
@ -960,6 +1160,11 @@ def test_lzw_bits():
def test_extents():
with Image.open("Tests/images/test_extents.gif") as im:
assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
im.seek(1)
assert im.size == (150, 150)

View File

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

View File

@ -1,4 +1,5 @@
import io
import os
import pytest
@ -70,6 +71,53 @@ def test_save_to_bytes():
)
def test_no_duplicates(tmp_path):
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
im = hopper()
sizes = [(32, 32), (64, 64)]
im.save(temp_file, "ico", sizes=sizes)
sizes.append(sizes[-1])
im.save(temp_file2, "ico", sizes=sizes)
assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
def test_different_bit_depths(tmp_path):
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
im = hopper()
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
hopper("1").save(
temp_file2,
"ico",
bitmap_format="bmp",
sizes=[(128, 128)],
append_images=[im],
)
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
# Test that only matching sizes of different bit depths are saved
temp_file3 = str(tmp_path / "temp3.ico")
temp_file4 = str(tmp_path / "temp4.ico")
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
im.save(
temp_file4,
"ico",
bitmap_format="bmp",
sizes=[(128, 128)],
append_images=[Image.new("P", (64, 64))],
)
assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4)
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode):
output = io.BytesIO()

View File

@ -68,6 +68,13 @@ class TestFileJpeg:
assert im.format == "JPEG"
assert im.get_format_mimetype() == "image/jpeg"
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero(self, size, tmp_path):
f = str(tmp_path / "temp.jpg")
im = Image.new("RGB", size)
with pytest.raises(ValueError):
im.save(f)
def test_app(self):
# Test APP/COM reader (@PIL135)
with Image.open(TEST_FILE) as im:
@ -736,7 +743,7 @@ class TestFileJpeg:
# Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated."
# http://www.exiv2.org/tags.html
# https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self):

View File

@ -209,6 +209,49 @@ def test_layers():
assert_image_similar(im, test_card, 0.4)
@pytest.mark.parametrize(
"name, args, offset, data",
(
("foo.j2k", {}, 0, b"\xff\x4f"),
("foo.jp2", {}, 4, b"jP"),
(None, {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
),
)
def test_no_jp2(name, args, offset, data):
out = BytesIO()
if name:
out.name = name
test_card.save(out, "JPEG2000", **args)
out.seek(offset)
assert out.read(2) == data
def test_mct():
# Three component
for val in (0, 1):
out = BytesIO()
test_card.save(out, "JPEG2000", mct=val, no_jp2=True)
assert out.getvalue()[59] == val
with Image.open(out) as im:
assert_image_similar(im, test_card, 1.0e-3)
# Single component should have MCT disabled
for val in (0, 1):
out = BytesIO()
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
jp2.save(out, "JPEG2000", mct=val, no_jp2=True)
assert out.getvalue()[53] == 0
with Image.open(out) as im:
assert_image_similar(im, jp2, 1.0e-3)
def test_rgba():
# Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
@ -255,6 +298,11 @@ def test_16bit_jp2_roundtrips():
assert_image_equal(im, jp2)
def test_issue_6194():
with Image.open("Tests/images/issue_6194.j2k") as im:
assert im.getpixel((5, 5)) == 31
def test_unbound_local():
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError):

View File

@ -4,7 +4,6 @@ import itertools
import os
import re
from collections import namedtuple
from ctypes import c_float
import pytest
@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase):
val = original[tag]
if tag.endswith("Resolution"):
if legacy_api:
assert (
c_float(val[0][0] / val[0][1]).value
== c_float(value[0][0] / value[0][1]).value
assert val[0][0] / val[0][1] == (
4294967295 / 113653537
), f"{tag} didn't roundtrip"
else:
assert (
c_float(val).value == c_float(value).value
), f"{tag} didn't roundtrip"
assert val == 37.79000115940079, f"{tag} didn't roundtrip"
else:
assert val == value, f"{tag} didn't roundtrip"
@ -501,8 +497,8 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, compression="tiff_adobe_deflate")
assert_image_equal_tofile(im, out)
def test_palette_save(self, tmp_path):
im = hopper("P")
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
def test_palette_save(self, im, tmp_path):
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True

View File

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

View File

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

View File

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

View File

@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
def test_sanity():
with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "RGB"
assert im.size == (128, 128)
assert im.format, "PPM"
assert im.format == "PPM"
assert im.get_format_mimetype() == "image/x-portable-pixmap"
@pytest.mark.parametrize(
"data, mode, pixels",
(
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
# P6 with maxval < 255
(
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
"RGB",
(
(0, 15, 30),
(120, 135, 150),
(225, 240, 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"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
"RGB",
(
(0, 1, 2),
(127, 128, 129),
(254, 255, 255),
),
),
),
)
def test_arbitrary_maxval(data, mode, pixels):
fp = BytesIO(data)
with Image.open(fp) as im:
assert im.size == (3, 1)
assert im.mode == mode
px = im.load()
assert tuple(px[x, 0] for x in range(3)) == pixels
def test_16bit_pgm():
with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
assert im.mode == "I"
assert im.size == (20, 100)
assert im.get_format_mimetype() == "image/x-portable-graymap"
@ -32,8 +69,6 @@ def test_16bit_pgm():
def test_16bit_pgm_write(tmp_path):
with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
f = str(tmp_path / "temp.pgm")
im.save(f, "PPM")
@ -82,17 +117,6 @@ def test_16bit_plain_pgm(tmp_path):
assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
def test_32bit_plain_pgm(tmp_path):
# P2 with maxval 2 ** 31 - 1
with Image.open("Tests/images/hopper_32bit_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 ** 31 - 1
assert_image_equal_tofile(im, "Tests/images/hopper_32bit.pgm")
def test_plain_pbm_data_with_comments(tmp_path):
path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm")
@ -222,22 +246,11 @@ def test_header_token_too_long(tmp_path):
with Image.open(path):
pass
assert str(e.value) == "Token too long in file header: b'01234567890'"
def test_too_many_colors(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n1 1\n1000\n")
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Too many colors for band: 1000"
assert str(e.value) == "Token too long in file header: 01234567890"
def test_truncated_header(tmp_path):
# Test EOF in header
path = str(tmp_path / "temp.pgm")
with open(path, "w") as f:
f.write("P6")
@ -248,6 +261,25 @@ def test_truncated_header(tmp_path):
assert str(e.value) == "Reached EOF while reading header"
# Test EOF for PyDecoder
fp = BytesIO(b"P5 3 1 4")
with Image.open(fp) as im:
with pytest.raises(ValueError):
im.load()
@pytest.mark.parametrize("maxval", (0, 65536))
def test_invalid_maxval(maxval, tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "w") as f:
f.write("P6\n3 1 " + str(maxval))
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the

View File

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

View File

@ -70,6 +70,15 @@ class TestFileTiff:
im.load()
im.close()
def test_seek_after_close(self):
im = Image.open("Tests/images/multipage.tiff")
im.close()
with pytest.raises(ValueError):
im.n_frames
with pytest.raises(ValueError):
im.seek(1)
def test_context_manager(self):
with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im:
@ -87,18 +96,38 @@ class TestFileTiff:
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
def test_bigtiff(self):
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
@pytest.mark.parametrize(
"file_name,mode,size,offset",
"file_name,mode,size,tile",
[
("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160),
("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8),
(
"tiff_wrong_bits_per_sample.tiff",
"RGBA",
(52, 53),
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
),
(
"tiff_wrong_bits_per_sample_2.tiff",
"RGB",
(16, 16),
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
),
(
"tiff_wrong_bits_per_sample_3.tiff",
"RGBA",
(512, 256),
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
),
],
)
def test_wrong_bits_per_sample(self, file_name, mode, size, offset):
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
assert im.size == size
assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))]
assert im.tile == tile
im.load()
def test_set_legacy_api(self):
@ -147,14 +176,14 @@ class TestFileTiff:
assert im.info["dpi"] == (71.0, 71.0)
@pytest.mark.parametrize(
"resolutionUnit, dpi",
"resolution_unit, dpi",
[(None, 72.8), (2, 72.8), (3, 184.912)],
)
def test_load_float_dpi(self, resolutionUnit, dpi):
def test_load_float_dpi(self, resolution_unit, dpi):
with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif"
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im:
assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
assert im.info["dpi"] == (dpi, dpi)
def test_save_float_dpi(self, tmp_path):
@ -221,6 +250,15 @@ class TestFileTiff:
assert b[0] == ord(b"\x01")
assert b[1] == ord(b"\xe0")
def test_16bit_r(self):
with Image.open("Tests/images/16bit.r.tif") as im:
assert im.getpixel((0, 0)) == 480
assert im.mode == "I;16"
b = im.tobytes()
assert b[0] == ord(b"\xe0")
assert b[1] == ord(b"\x01")
def test_16bit_s(self):
with Image.open("Tests/images/16bit.s.tif") as im:
im.load()
@ -459,6 +497,26 @@ class TestFileTiff:
exif = im.getexif()
check_exif(exif)
def test_modify_exif(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
exif = im.getexif()
exif[256] = 100
im.save(outfile, exif=exif)
with Image.open(outfile) as im:
exif = im.getexif()
assert exif[256] == 100
def test_reload_exif_after_seek(self):
with Image.open("Tests/images/multipage.tiff") as im:
exif = im.getexif()
del exif[256]
im.seek(1)
assert 256 in exif
def test_exif_frames(self):
# Test that EXIF data can change across frames
with Image.open("Tests/images/g4-multi.tiff") as im:
@ -598,6 +656,17 @@ class TestFileTiff:
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
def test_planar_configuration_save(self, tmp_path):
infile = "Tests/images/tiff_tiled_planar_raw.tif"
with Image.open(infile) as im:
assert im._planar_configuration == 2
outfile = str(tmp_path / "temp.tif")
im.save(outfile)
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)
def test_palette(self, tmp_path):
def roundtrip(mode):
outfile = str(tmp_path / "temp.tif")
@ -631,11 +700,11 @@ class TestFileTiff:
assert reread.n_frames == 3
# Test appending using a generator
def imGenerator(ims):
def im_generator(ims):
yield from ims
mp = BytesIO()
im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims))
im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
@ -666,6 +735,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert reloaded.info["icc_profile"] == icc_profile
def test_save_bmp_compression(self, tmp_path):
with Image.open("Tests/images/hopper.bmp") as im:
assert im.info["compression"] == 0
outfile = str(tmp_path / "temp.tif")
im.save(outfile)
def test_discard_icc_profile(self, tmp_path):
outfile = str(tmp_path / "temp.tif")

View File

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

View File

@ -8,6 +8,7 @@ import pytest
from PIL import Image, WebPImagePlugin, features
from .helper import (
assert_image_equal,
assert_image_similar,
assert_image_similar_tofile,
hopper,
@ -105,6 +106,19 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@skip_unless_feature("webp_anim")
def test_save_all(self, tmp_path):
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im.save(temp_file, save_all=True, append_images=[im2])
with Image.open(temp_file) as reloaded:
assert_image_equal(im, reloaded)
reloaded.seek(1)
assert_image_similar(im2, reloaded, 1)
def test_icc_profile(self, tmp_path):
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM:
@ -171,9 +185,25 @@ class TestFileWebp:
Image.open(blob).load()
Image.open(blob).load()
@skip_unless_feature("webp")
@pytest.mark.parametrize(
"background",
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
)
@skip_unless_feature("webp_anim")
def test_invalid_background(self, background, tmp_path):
temp_file = str(tmp_path / "temp.webp")
im = hopper()
with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background)
@skip_unless_feature("webp_anim")
def test_background_from_gif(self, tmp_path):
# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
# Save P mode GIF with background
with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1))
@ -191,7 +221,6 @@ class TestFileWebp:
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
assert difference < 5
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_duration(self, tmp_path):
with Image.open("Tests/images/dispose_bgnd.gif") as im:

View File

@ -1,6 +1,7 @@
import pytest
from packaging.version import parse as parse_version
from PIL import Image
from PIL import Image, features
from .helper import (
assert_image_equal,
@ -27,7 +28,6 @@ def test_n_frames():
assert im.is_animated
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_write_animation_L(tmp_path):
"""
Convert an animated GIF to animated WebP, then compare the frame count, and first
@ -46,6 +46,11 @@ def test_write_animation_L(tmp_path):
orig.load()
im.load()
assert_image_similar(im, orig.convert("RGBA"), 32.9)
if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1)
orig.load()
@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path):
assert_image_similar(im, orig.convert("RGBA"), 32.9)
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_write_animation_RGB(tmp_path):
"""
Write an animated WebP from RGB frames, and ensure the frames
@ -69,6 +73,10 @@ def test_write_animation_RGB(tmp_path):
assert_image_equal(im, frame1.convert("RGBA"))
# Compare second frame to original
if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1)
im.load()
assert_image_equal(im, frame2.convert("RGBA"))
@ -82,14 +90,14 @@ def test_write_animation_RGB(tmp_path):
check(temp_file1)
# Tests appending using a generator
def imGenerator(ims):
def im_generator(ims):
yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp")
frame1.copy().save(
temp_file2,
save_all=True,
append_images=imGenerator([frame2]),
append_images=im_generator([frame2]),
lossless=True,
)
check(temp_file2)

View File

@ -7,7 +7,7 @@ import warnings
import pytest
from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError
from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
from .helper import (
assert_image_equal,
@ -161,6 +161,8 @@ class TestImage:
assert im.size == (128, 128)
for ext in (".jpg", ".jp2"):
if ext == ".jp2" and not features.check_codec("jpg_2000"):
pytest.skip("jpg_2000 not available")
temp_file = str(tmp_path / ("temp." + ext))
if os.path.exists(temp_file):
os.remove(temp_file)
@ -170,7 +172,7 @@ class TestImage:
temp_file = str(tmp_path / "temp.jpg")
class FP:
def write(a, b):
def write(self, b):
pass
fp = FP()
@ -602,11 +604,34 @@ class TestImage:
with Image.open("Tests/images/hopper.gif") as im:
assert_image_equal(im, im.remap_palette(list(range(256))))
# Test identity transform with an RGBA palette
im = Image.new("P", (256, 1))
for x in range(256):
im.putpixel((x, 0), x)
im.putpalette(list(range(256)) * 4, "RGBA")
im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped)
assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode
with hopper() as im:
with pytest.raises(ValueError):
im.remap_palette(None)
def test_remap_palette_transparency(self):
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
im.info["transparency"] = 0
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
# Test unused transparency
im.info["transparency"] = 2
im_remapped = im.remap_palette([1, 0])
assert "transparency" not in im_remapped.info
def test__new(self):
im = hopper("RGB")
im_p = hopper("P")
@ -652,6 +677,15 @@ class TestImage:
with warnings.catch_warnings():
im.save(temp_file)
def test_no_new_file_on_error(self, tmp_path):
temp_file = str(tmp_path / "temp.jpg")
im = Image.new("RGB", (0, 0))
with pytest.raises(ValueError):
im.save(temp_file)
assert not os.path.exists(temp_file)
def test_load_on_nonexclusive_multiframe(self):
with open("Tests/images/frozenpond.mpo", "rb") as fp:
@ -666,6 +700,19 @@ class TestImage:
assert not fp.closed
def test_empty_exif(self):
with Image.open("Tests/images/exif.png") as im:
exif = im.getexif()
assert dict(exif) != {}
# Test that exif data is cleared after another load
exif.load(None)
assert dict(exif) == {}
# Test loading just the EXIF header
exif.load(b"Exif\x00\x00")
assert dict(exif) == {}
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@ -802,6 +849,35 @@ class TestImage:
im = Image.new("RGB", size)
assert im.tobytes() == b""
def test_apply_transparency(self):
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1))
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
# Test that no transformation is applied without transparency
im.apply_transparency()
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
# Test that a transparency index is applied
im.info["transparency"] = 0
im.apply_transparency()
assert "transparency" not in im.info
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1}
# Test that existing transparency is kept
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
im.info["transparency"] = 0
im.apply_transparency()
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1}
# Test that transparency bytes are applied
with Image.open("Tests/images/pil123p.png") as im:
assert isinstance(im.info["transparency"], bytes)
assert im.palette.colors[(27, 35, 6)] == 24
im.apply_transparency()
assert im.palette.colors[(27, 35, 6, 214)] == 24
def test_categories_deprecation(self):
with pytest.warns(DeprecationWarning):
assert hopper().category == 0

View File

@ -1,4 +1,3 @@
import ctypes
import os
import subprocess
import sys
@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest):
# Check 0
im = Image.new(mode, (0, 0), None)
with pytest.raises(IndexError):
assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
im.putpixel((0, 0), c)
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
with pytest.raises(IndexError):
with pytest.raises(error):
im.putpixel((-1, -1), c)
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((-1, -1))
# check initial color
@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest):
# Check 0
im = Image.new(mode, (0, 0), c)
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
with pytest.raises(IndexError):
with pytest.raises(error):
im.getpixel((-1, -1))
def test_basic(self):
@ -401,6 +403,8 @@ class TestEmbeddable:
"not from shell",
)
def test_embeddable(self):
import ctypes
with open("embed_pil.c", "w") as fh:
fh.write(
"""

View File

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

View File

@ -27,15 +27,15 @@ def test_sanity():
"HSV",
)
for mode in modes:
im = hopper(mode)
for mode in modes:
convert(im, mode)
for input_mode in modes:
im = hopper(input_mode)
for output_mode in modes:
convert(im, output_mode)
# Check 0
im = Image.new(mode, (0, 0))
for mode in modes:
convert(im, mode)
im = Image.new(input_mode, (0, 0))
for output_mode in modes:
convert(im, output_mode)
def test_default():
@ -70,6 +70,11 @@ def test_16bit():
with Image.open("Tests/images/16bit.cropped.tif") as im:
_test_float_conversion(im)
for color in (65535, 65536):
im = Image.new("I", (1, 1), color)
im_i16 = im.convert("I;16")
assert im_i16.getpixel((0, 0)) == 65535
def test_16bit_workaround():
with Image.open("Tests/images/16bit.cropped.tif") as im:
@ -135,6 +140,10 @@ def test_trns_l(tmp_path):
f = str(tmp_path / "temp.png")
im_la = im.convert("LA")
assert "transparency" not in im_la.info
im_la.save(f)
im_rgb = im.convert("RGB")
assert im_rgb.info["transparency"] == (128, 128, 128) # undone
im_rgb.save(f)
@ -213,6 +222,20 @@ def test_p_la():
assert_image_similar(alpha, comparable, 5)
def test_p2pa_alpha():
with Image.open("Tests/images/tiny.png") as im:
assert im.mode == "P"
im_pa = im.convert("PA")
assert im_pa.mode == "PA"
im_a = im_pa.getchannel("A")
for x in range(4):
alpha = 255 if x > 1 else 0
for y in range(4):
assert im_a.getpixel((x, y)) == alpha
def test_matrix_illegal_conversion():
# Arrange
im = hopper("CMYK")

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
from PIL import Image
from .helper import assert_image_equal, cached_property
from .helper import CachedProperty, assert_image_equal
class TestImagingPaste:
@ -34,7 +34,7 @@ class TestImagingPaste:
im.paste(im2, mask)
self.assert_9points_image(im, expected)
@cached_property
@CachedProperty
def mask_1(self):
mask = Image.new("1", (self.size, self.size))
px = mask.load()
@ -43,11 +43,11 @@ class TestImagingPaste:
px[y, x] = (x + y) % 2
return mask
@cached_property
@CachedProperty
def mask_L(self):
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@cached_property
@CachedProperty
def gradient_L(self):
gradient = Image.new("L", (self.size, self.size))
px = gradient.load()
@ -56,7 +56,7 @@ class TestImagingPaste:
px[y, x] = (x + y) % 255
return gradient
@cached_property
@CachedProperty
def gradient_RGB(self):
return Image.merge(
"RGB",
@ -67,7 +67,17 @@ class TestImagingPaste:
],
)
@cached_property
@CachedProperty
def gradient_LA(self):
return Image.merge(
"LA",
[
self.gradient_L,
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
],
)
@CachedProperty
def gradient_RGBA(self):
return Image.merge(
"RGBA",
@ -79,7 +89,7 @@ class TestImagingPaste:
],
)
@cached_property
@CachedProperty
def gradient_RGBa(self):
return Image.merge(
"RGBa",
@ -145,6 +155,28 @@ class TestImagingPaste:
],
)
def test_image_mask_LA(self):
for mode in ("RGBA", "RGB", "L"):
im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode)
self.assert_9points_paste(
im,
im2,
self.gradient_LA,
[
(128, 191, 255, 191),
(112, 207, 206, 111),
(128, 254, 128, 1),
(208, 208, 239, 239),
(192, 191, 191, 191),
(207, 207, 112, 113),
(255, 255, 255, 255),
(239, 207, 207, 239),
(255, 191, 128, 191),
],
)
def test_image_mask_RGBA(self):
for mode in ("RGBA", "RGB", "L"):
im = Image.new(mode, (200, 200), "white")

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ from .helper import (
assert_image_similar,
fromstring,
hopper,
skip_unless_feature,
tostring,
)
@ -88,6 +89,15 @@ def test_no_resize():
assert im.size == (64, 64)
@skip_unless_feature("libtiff")
def test_load_first():
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations
with Image.open("Tests/images/g4_orientation_5.tif") as im:
im.thumbnail((64, 64))
assert im.size == (64, 10)
# valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing")
def test_DCT_scaling_edges():
@ -130,4 +140,4 @@ def test_reducing_gap_for_DCT_scaling():
with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
assert_image_equal(ref, im)
assert_image_similar(ref, im, 1.4)

View File

@ -655,6 +655,20 @@ def test_polygon_1px_high():
assert_image_equal_tofile(im, expected)
def test_polygon_1px_high_translucent():
# Test drawing a translucent 1px high polygon
# Arrange
im = Image.new("RGB", (4, 3))
draw = ImageDraw.Draw(im, "RGBA")
expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png"
# Act
draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127))
# Assert
assert_image_equal_tofile(im, expected)
def test_polygon_translucent():
# Arrange
im = Image.new("RGB", (W, H))
@ -1440,3 +1454,23 @@ def test_continuous_horizontal_edges_polygon():
assert_image_equal_tofile(
img, expected, "continuous horizontal edges polygon failed"
)
def test_discontiguous_corners_polygon():
img, draw = create_base_image_draw((84, 68))
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
draw.polygon(
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
BLACK,
)
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
assert_image_similar_tofile(img, expected, 1)
def test_polygon():
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
expected = "Tests/images/imagedraw_outline_polygon_RGB.png"
assert_image_similar_tofile(im, expected, 1)

View File

@ -2,7 +2,15 @@ from io import BytesIO
import pytest
from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features
from PIL import (
BmpImagePlugin,
EpsImagePlugin,
Image,
ImageFile,
UnidentifiedImageError,
_binary,
features,
)
from .helper import (
assert_image,
@ -35,9 +43,9 @@ class TestImageFile:
parser = ImageFile.Parser()
parser.feed(data)
imOut = parser.close()
im_out = parser.close()
return im, imOut
return im, im_out
assert_image_equal(*roundtrip("BMP"))
im1, im2 = roundtrip("GIF")
@ -200,6 +208,9 @@ class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer):
return 1, 1, b""
def cleanup(self):
self.cleanup_called = True
xoff, yoff, xsize, ysize = 10, 20, 100, 100
@ -327,10 +338,12 @@ class TestPyEncoder(CodecsTest):
im = MockImageFile(buf)
fp = BytesIO()
self.encoder.cleanup_called = False
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
)
assert self.encoder.cleanup_called
with pytest.raises(ValueError):
ImageFile._save(
@ -372,3 +385,7 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)
def test_zero_height(self):
with pytest.raises(UnidentifiedImageError):
Image.open("Tests/images/zero_height.j2k")

View File

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

View File

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

View File

@ -63,6 +63,7 @@ def test_sanity():
ImageOps.grayscale(hopper("L"))
ImageOps.grayscale(hopper("RGB"))
ImageOps.invert(hopper("1"))
ImageOps.invert(hopper("L"))
ImageOps.invert(hopper("RGB"))

View File

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

View File

@ -2,10 +2,13 @@ import warnings
import pytest
from PIL import ImageQt
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(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
)

View File

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

View File

@ -26,6 +26,8 @@ def setup_module():
# setup tk
tk.Frame()
# root = tk.Tk()
except RuntimeError as v:
pytest.skip(f"RuntimeError: {v}")
except tk.TclError as v:
pytest.skip(f"TCL Error: {v}")
@ -75,8 +77,16 @@ def test_photoimage_blank():
assert im_tk.width() == 100
assert im_tk.height() == 100
# reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im)
im = Image.new(mode, (100, 100))
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded.convert(mode), im)
def test_box_deprecation():
im = hopper()
im_tk = ImageTk.PhotoImage(im)
with pytest.warns(DeprecationWarning):
im_tk.paste(im, (0, 0, 128, 128))
def test_bitmapimage():

View File

@ -1,4 +1,3 @@
import ctypes
from io import BytesIO
from PIL import Image, ImageWin
@ -8,6 +7,7 @@ from .helper import hopper, is_win32
# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
if is_win32():
import ctypes
import ctypes.wintypes
class BITMAPFILEHEADER(ctypes.Structure):

View File

@ -444,6 +444,8 @@ class TestLibUnpack:
self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16))
self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15))
self.assert_unpack(
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
)

View File

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

View File

@ -1,6 +1,10 @@
import warnings
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

View File

@ -1,6 +1,10 @@
import warnings
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

View File

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

View File

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

View File

@ -16,8 +16,6 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
# sys.path.insert(0, os.path.abspath('.'))
import sphinx_rtd_theme
import PIL
# -- General configuration ------------------------------------------------
@ -70,7 +68,7 @@ release = PIL.__version__
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@ -126,13 +124,15 @@ nitpicky = True
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_theme = "furo"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
html_theme_options = {
"light_logo": "pillow-logo-dark-text.png",
"dark_logo": "pillow-logo.png",
}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
@ -146,7 +146,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = "resources/pillow-logo.png"
# html_logo = "resources/pillow-logo.png"
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@ -311,10 +311,7 @@ texinfo_documents = [
def setup(app):
app.add_js_file("js/script.js")
app.add_css_file("css/styles.css")
app.add_css_file("css/dark.css")
app.add_css_file("css/light.css")
# GitHub repo for sphinx-issues

View File

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

View File

@ -253,7 +253,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e:
raise OSError("Truncated DDS file") from e
return 0, 0
return -1, 0
class DXT5Decoder(ImageFile.PyDecoder):
@ -264,7 +264,7 @@ class DXT5Decoder(ImageFile.PyDecoder):
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e:
raise OSError("Truncated DDS file") from e
return 0, 0
return -1, 0
Image.register_decoder("DXT1", DXT1Decoder)

View File

@ -17,15 +17,13 @@ When an image is opened from a file, only that instance of the image is consider
have the format. Copies of the image will contain data loaded from the file, but not
the file itself, meaning that it can no longer be considered to be in the original
format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
internally creates a copy of the image, the ``fp`` (file pointer), along with any
methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format`
attribute will be ``None``.
internally creates a copy of the image, then any methods or attributes specific to the
format will no longer be present. The ``fp`` (file pointer) attribute will no longer be
present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
Fully supported formats
-----------------------
.. contents::
BLP
^^^
@ -44,8 +42,9 @@ BMP
^^^
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding
is not supported.
or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
is not supported. Support for reading 8-bit run-length encoding was added in Pillow
9.1.0.
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -102,12 +101,38 @@ GIF
^^^
Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
writes run-length encoded files in GIF87a by default, unless GIF89a features
are used or GIF89a is already in use.
writes files in GIF87a by default, unless GIF89a features are used or GIF89a is
already in use. Files are written with LZW encoding.
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
images, but seeking to later frames in an image will change the mode to either
``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
images. Seeking to later frames in a ``P`` image will change the image to
``RGB`` (or ``RGBA`` if the first frame had transparency).
``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain
its own individual palette of up to 256 colors. When a new frame is placed onto a
previous frame, those colors may combine to exceed the ``P`` mode limit of 256
colors. Instead, the image is converted to ``RGB`` handle this.
If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that
every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting
available::
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
GIF frames do not always contain individual palettes however. If there is only
a global palette, then all of the colors can fit within ``P`` mode. If you would
prefer the frames to be kept as ``P`` in that case, there is also a setting
available::
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
To restore the default behavior, where ``P`` mode images are only converted to
``RGB`` or ``RGBA`` after the first frame::
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@ -131,7 +156,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
it will loop forever.
**comment**
May not be present. A comment about the image.
May not be present. A comment about the image. This is the last comment found
before the current frame's image.
**extension**
May not be present. Contains application specific information.
@ -220,17 +246,14 @@ Reading local images
The GIF loader creates an image memory the same size as the GIF files *logical
screen size*, and pastes the actual pixel data (the *local image*) into this
image. If you only want the actual pixel rectangle, you can manipulate the
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.ImageFile.ImageFile.tile`
attributes before loading the file::
image. If you only want the actual pixel rectangle, you can crop the image::
im = Image.open(...)
if im.tile[0][0] == "gif":
# only read the first "local image" from this GIF file
tag, (x0, y0, x1, y1), offset, extra = im.tile[0]
im.size = (x1 - x0, y1 - y0)
im.tile = [(tag, (0, 0) + im.size, offset, extra)]
box = im.tile[0][1]
im = im.crop(box)
ICNS
^^^^
@ -364,10 +387,12 @@ The :py:meth:`~PIL.Image.open` method may set the following
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**quality**
The image quality, on a scale from 0 (worst) to 95 (best). The default is
75. Values above 95 should be avoided; 100 disables portions of the JPEG
compression algorithm, and results in large files with hardly any gain in
image quality.
The image quality, on a scale from 0 (worst) to 95 (best), or the string
``keep``. The default is 75. Values above 95 should be avoided; 100 disables
portions of the JPEG compression algorithm, and results in large files with
hardly any gain in image quality. The value ``keep`` is only valid for JPEG
files and will retain the original image quality level, subsampling, and
qtables.
**optimize**
If present and true, indicates that the encoder should make an extra pass
@ -475,9 +500,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
and must be greater than the code-block size.
**irreversible**
If ``True``, use the lossy Irreversible Color Transformation
followed by DWT 9-7. Defaults to ``False``, which means to use the
Reversible Color Transformation with DWT 5-3.
If ``True``, use the lossy discrete waveform transformation DWT 9-7.
Defaults to ``False``, which uses the lossless DWT 5-3.
**mct**
If ``1`` then enable multiple component transformation when encoding,
otherwise use ``0`` for no component transformation (default). If MCT is
enabled and ``irreversible`` is ``True`` then the Irreversible Color
Transformation will be applied, otherwise encoding will use the
Reversible Color Transformation. MCT works best with a ``mode`` of
``RGB`` and is only applicable when the image data has 3 components.
.. versionadded:: 9.1.0
**progression**
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
@ -497,6 +531,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
for compliant 4K files, *at least one* of the dimensions must match
4096 x 2160.
**no_jp2**
If ``True`` then don't wrap the raw codestream in the JP2 file format when
saving, otherwise the extension of the filename will be used to determine
the format (default).
.. versionadded:: 9.1.0
.. note::
To enable JPEG 2000 support, you need to build and install the OpenJPEG
@ -743,7 +784,7 @@ parameter must be set to ``True``. The following parameters can also be set:
PPM
^^^
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
``RGB`` data.
SGI
@ -1192,6 +1233,11 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
SUN
^^^
Pillow identifies and reads Sun raster files.
WAL
^^^
@ -1206,13 +1252,13 @@ this format.
By default, a Quake2 standard palette is attached to the texture. To override
the palette, use the putpalette method.
WMF
^^^
WMF, EMF
^^^^^^^^
Pillow can identify WMF files.
Pillow can identify WMF and EMF files.
On Windows, it can read WMF files. By default, it will load the image at 72
dpi. To load it at another resolution:
On Windows, it can read WMF and EMF files. By default, it will load the image
at 72 dpi. To load it at another resolution:
.. code-block:: python
@ -1222,7 +1268,8 @@ dpi. To load it at another resolution:
im.load(dpi=144)
To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler.
.. code-block:: python

View File

@ -171,20 +171,37 @@ Rolling an image
::
def roll(image, delta):
def roll(im, delta):
"""Roll an image sideways."""
xsize, ysize = image.size
xsize, ysize = im.size
delta = delta % xsize
if delta == 0:
return image
return im
part1 = image.crop((0, 0, delta, ysize))
part2 = image.crop((delta, 0, xsize, ysize))
image.paste(part1, (xsize - delta, 0, xsize, ysize))
image.paste(part2, (0, 0, xsize - delta, ysize))
part1 = im.crop((0, 0, delta, ysize))
part2 = im.crop((delta, 0, xsize, ysize))
im.paste(part1, (xsize - delta, 0, xsize, ysize))
im.paste(part2, (0, 0, xsize - delta, ysize))
return image
return im
Or if you would like to merge two images into a wider image:
Merging images
^^^^^^^^^^^^^^
::
def merge(im1, im2):
w = im1.size[0] + im2.size[0]
h = max(im1.size[1], im2.size[1])
im = Image.new("RGBA", (w, h))
im.paste(im1)
im.paste(im2, (im1.size[0], 0))
return im
For more advanced tricks, the paste method can also take a transparency mask as
an optional argument. In this mask, the value 255 indicates that the pasted
@ -487,6 +504,17 @@ image header. In addition, seek will also be used when the image data is read
tar file, you can use the :py:class:`~PIL.ContainerIO` or
:py:class:`~PIL.TarIO` modules to access it.
Reading from URL
^^^^^^^^^^^^^^^^
::
from PIL import Image
from urllib.request import urlopen
url = "https://python-pillow.org/images/pillow-logo.png"
img = Image.open(urlopen(url))
Reading from a tar archive
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -123,8 +123,12 @@ The ``tile`` attribute
To be able to read the file as well as just identifying it, the ``tile``
attribute must also be set. This attribute consists of a list of tile
descriptors, where each descriptor specifies how data should be loaded to a
given region in the image. In most cases, only a single descriptor is used,
covering the full image.
given region in the image.
In most cases, only a single descriptor is used, covering the full image.
:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine
channels within a single layer, given that the channels are stored separately,
one after the other.
The tile descriptor is a 4-tuple with the following contents::
@ -324,42 +328,42 @@ The fields are used as follows:
Whether the first line in the image is the top line on the screen (1), or
the bottom line (-1). If omitted, the orientation defaults to 1.
.. _file-decoders:
.. _file-codecs:
Writing Your Own File Decoder in C
==================================
Writing Your Own File Codec in C
================================
There are 3 stages in a file decoder's lifetime:
There are 3 stages in a file codec's lifetime:
1. Setup: Pillow looks for a function in the decoder registry, falling
back to a function named ``[decodername]_decoder`` on the internal
core image object. That function is called with the ``args`` tuple
from the ``tile`` setup in the ``_open`` method.
1. Setup: Pillow looks for a function in the decoder or encoder registry,
falling back to a function named ``[codecname]_decoder`` or
``[codecname]_encoder`` on the internal core image object. That function is
called with the ``args`` tuple from the ``tile``.
2. Decoding: The decoder's decode function is repeatedly called with
chunks of image data.
2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
called with chunks of image data.
3. Cleanup: If the decoder has registered a cleanup function, it will
be called at the end of the decoding process, even if there was an
3. Cleanup: If the codec has registered a cleanup function, it will
be called at the end of the transformation process, even if there was an
exception raised.
Setup
-----
The current conventions are that the decoder setup function is named
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The
python binding for it is named ``[decodername]_decoder`` and is setup
from within the ``_imaging.c`` file in the codecs section of the
function array.
The current conventions are that the codec setup function is named
``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
within the ``_imaging.c`` file in the codecs section of the function array.
The setup function needs to call ``PyImaging_DecoderNew`` and at the
very least, set the ``decode`` function pointer. The fields of
interest in this object are:
The setup function needs to call ``PyImaging_DecoderNew`` or
``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
``encode`` function pointer. The fields of interest in this object are:
**decode**
Function pointer to the decode function, which has access to
``im``, ``state``, and the buffer of data to be added to the image.
**decode**/**encode**
Function pointer to the decode or encode function, which has access to
``im``, ``state``, and the buffer of data to be transformed.
**cleanup**
Function pointer to the cleanup function, has access to ``state``.
@ -369,36 +373,34 @@ interest in this object are:
**state**
An ImagingCodecStateInstance, will be set by Pillow. The ``context``
member is an opaque struct that can be used by the decoder to store
member is an opaque struct that can be used by the codec to store
any format specific state or options.
**pulls_fd**
**EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1,
``state->fd`` will be a pointer to the Python file like object. The
decoder may use the functions in ``codec_fd.c`` to read directly
from the file like object rather than have the data pushed through a
buffer. Note that this implementation may be refactored until this
warning is removed.
**pulls_fd**/**pushes_fd**
If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1,
``state->fd`` will be a pointer to the Python file like object. The codec may
use the functions in ``codec_fd.c`` to read or write directly with the file
like object rather than have the data pushed through a buffer.
.. versionadded:: 3.3.0
Decoding
--------
Transforming
------------
The decode function is called with the target (core) image, the
decoder state structure, and a buffer of data to be decoded.
The decode or encode function is called with the target (core) image, the codec
state structure, and a buffer of data to be transformed.
**Experimental** -- If ``pulls_fd`` is set, then the decode function
is called once, with an empty buffer. It is the decoder's
responsibility to decode the entire tile in that one call. The rest of
this section only applies if ``pulls_fd`` is not set.
It is the codec's responsibility to pull as much data as possible out of the
buffer and return the number of bytes consumed. The next call to the codec will
include the previous unconsumed tail. The codec function will be called
multiple times as the data processed.
It is the decoder's responsibility to pull as much data as possible
out of the buffer and return the number of bytes consumed. The next
call to the decoder will include the previous unconsumed tail. The
decoder function will be called multiple times as the data is read
from the file like object.
Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
encode function is called once, with an empty buffer. It is the codec's
responsibility to transform the entire tile in that one call. Using this will
provide a codec with more freedom, but that freedom may mean increased memory
usage if the entire tile is held in memory at once by the codec.
If an error occurs, set ``state->errcode`` and return -1.
@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
Cleanup
-------
The cleanup function is called after the decoder returns a negative
value, or if there is a read error from the file. This function should
free any allocated memory and release any resources from external
libraries.
The cleanup function is called after the codec returns a negative
value, or if there is an error. This function should free any allocated
memory and release any resources from external libraries.
.. _file-codecs-py:
@ -425,11 +426,32 @@ They should be registered using :py:meth:`PIL.Image.register_decoder` and
the file codecs, there are three stages in the lifetime of a
Python-based file codec:
1. Setup: Pillow looks for the decoder in the registry, then
1. Setup: Pillow looks for the codec in the decoder or encoder registry, then
instantiates the class.
2. Transforming: The instance's ``decode`` method is repeatedly called with
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
called with the size of data to be output.
3. Cleanup: The instance's ``cleanup`` method is called.
Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's
``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode``
will only be called once. In the decoder, ``self.fd`` can be used to access
the file-like object. Using this will provide a codec with more freedom, but
that freedom may mean increased memory usage if entire file is held in
memory at once by the codec.
In ``decode``, once the data has been interpreted, ``set_as_raw`` can be
used to populate the image.
3. Cleanup: The instance's ``cleanup`` method is called once the transformation
is complete. This can be used to clean up any resources used by the codec.
If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you
probably chose to perform any cleanup tasks at the end of ``decode`` or
``encode``.
For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin
<https://github.com/python-pillow/Pillow/blob/main/docs/example/DdsImagePlugin.py>`_.
For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and
:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin
<https://github.com/python-pillow/Pillow/blob/main/src/PIL/BlpImagePlugin.py>`_

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