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 - mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs9550w32.exe /S - ..\pillow-depends\gs9561w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH% - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
@ -43,7 +43,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - 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% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' - '%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 # gather the coverage data
python3 -m pip install codecov python3 -m pip install codecov
if [[ $MATRIX_DOCKER ]]; then if [[ $MATRIX_DOCKER ]]; then
coverage xml --ignore-errors python3 -m coverage xml --ignore-errors
else else
coverage xml python3 -m coverage xml
fi fi

View File

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

View File

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

View File

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

View File

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

1
.github/mergify.yml vendored
View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ jobs:
python-version: [ python-version: [
"pypy-3.8", "pypy-3.8",
"pypy-3.7", "pypy-3.7",
"3.11-dev",
"3.10", "3.10",
"3.9", "3.9",
"3.8", "3.8",
@ -59,6 +60,8 @@ jobs:
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')
run: | run: |
.github/workflows/macos-install.sh .github/workflows/macos-install.sh
env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Build - name: Build
run: | run: |
@ -93,7 +96,7 @@ jobs:
- name: Docs - name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: | 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 make doccheck
- name: After success - name: After success

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -24,13 +24,12 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create and check source distribution: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
twine check dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.: * [ ] Check and upload all binaries and source distributions e.g.:
```bash ```bash
twine check dist/* python3 -m twine check --strict dist/*
twine upload dist/Pillow-5.2.0* python3 -m twine upload dist/Pillow-5.2.0*
``` ```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] 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` * [ ] 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
twine check dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.: * [ ] Check and upload all binaries and source distributions e.g.:
```bash ```bash
twine check dist/* python3 -m twine check --strict dist/*
twine upload dist/Pillow-5.2.1* python3 -m twine upload dist/Pillow-5.2.1*
``` ```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
twine check dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)

View File

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

View File

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

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

View File

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

View File

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

91
Tests/test_deprecate.py Normal file
View File

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

View File

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

View File

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

View File

@ -4,7 +4,12 @@ import pytest
from PIL import BmpImagePlugin, Image 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): 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") 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(): def test_offset():
# This image has been hexedited # This image has been hexedited
# to exclude the palette size from the pixel data offset # to exclude the palette size from the pixel data offset

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import io import io
import os
import pytest 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")) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode): def test_save_to_bytes_bmp(mode):
output = io.BytesIO() output = io.BytesIO()

View File

@ -68,6 +68,13 @@ class TestFileJpeg:
assert im.format == "JPEG" assert im.format == "JPEG"
assert im.get_format_mimetype() == "image/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): def test_app(self):
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
@ -736,7 +743,7 @@ class TestFileJpeg:
# Act / Assert # Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated." # "When the image resolution is unknown, 72 [dpi] is designated."
# http://www.exiv2.org/tags.html # https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self): def test_invalid_exif(self):

View File

@ -209,6 +209,49 @@ def test_layers():
assert_image_similar(im, test_card, 0.4) 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(): def test_rgba():
# Arrange # Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: 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) assert_image_equal(im, jp2)
def test_issue_6194():
with Image.open("Tests/images/issue_6194.j2k") as im:
assert im.getpixel((5, 5)) == 31
def test_unbound_local(): def test_unbound_local():
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception. # prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError): with pytest.raises(OSError):

View File

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

View File

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

View File

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

View File

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

View File

@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
def test_sanity(): def test_sanity():
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
assert im.size == (128, 128) assert im.size == (128, 128)
assert im.format, "PPM" assert im.format == "PPM"
assert im.get_format_mimetype() == "image/x-portable-pixmap" 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(): def test_16bit_pgm():
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
assert im.mode == "I" assert im.mode == "I"
assert im.size == (20, 100) assert im.size == (20, 100)
assert im.get_format_mimetype() == "image/x-portable-graymap" assert im.get_format_mimetype() == "image/x-portable-graymap"
@ -32,8 +69,6 @@ def test_16bit_pgm():
def test_16bit_pgm_write(tmp_path): def test_16bit_pgm_write(tmp_path):
with Image.open("Tests/images/16_bit_binary.pgm") as im: with Image.open("Tests/images/16_bit_binary.pgm") as im:
im.load()
f = str(tmp_path / "temp.pgm") f = str(tmp_path / "temp.pgm")
im.save(f, "PPM") 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") 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): def test_plain_pbm_data_with_comments(tmp_path):
path1 = str(tmp_path / "temp1.ppm") path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm") path2 = str(tmp_path / "temp2.ppm")
@ -222,22 +246,11 @@ def test_header_token_too_long(tmp_path):
with Image.open(path): with Image.open(path):
pass pass
assert str(e.value) == "Token too long in file header: b'01234567890'" assert str(e.value) == "Token too long in file header: 01234567890"
def test_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"
def test_truncated_header(tmp_path): def test_truncated_header(tmp_path):
# Test EOF in header
path = str(tmp_path / "temp.pgm") path = str(tmp_path / "temp.pgm")
with open(path, "w") as f: with open(path, "w") as f:
f.write("P6") f.write("P6")
@ -248,6 +261,25 @@ def test_truncated_header(tmp_path):
assert str(e.value) == "Reached EOF while reading header" 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(): def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the # 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: with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
with Image.open("Tests/images/cross_scan_line_truncated.tga") as im:
with pytest.raises(OSError):
im.load()
def test_save(tmp_path): def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga" test_file = "Tests/images/tga_id_field.tga"

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import pytest
from PIL import Image, WebPImagePlugin, features from PIL import Image, WebPImagePlugin, features
from .helper import ( from .helper import (
assert_image_equal,
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
@ -105,6 +106,19 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6) hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer() 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): def test_icc_profile(self, tmp_path):
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM: if _webp.HAVE_WEBPANIM:
@ -171,9 +185,25 @@ class TestFileWebp:
Image.open(blob).load() Image.open(blob).load()
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") @skip_unless_feature("webp_anim")
def test_background_from_gif(self, tmp_path): def test_background_from_gif(self, tmp_path):
# Save L mode GIF with background
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: with Image.open("Tests/images/chi.gif") as im:
original_value = im.convert("RGB").getpixel((1, 1)) 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)) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
assert difference < 5 assert difference < 5
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim") @skip_unless_feature("webp_anim")
def test_duration(self, tmp_path): def test_duration(self, tmp_path):
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:

View File

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

View File

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

View File

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

View File

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

View File

@ -27,15 +27,15 @@ def test_sanity():
"HSV", "HSV",
) )
for mode in modes: for input_mode in modes:
im = hopper(mode) im = hopper(input_mode)
for mode in modes: for output_mode in modes:
convert(im, mode) convert(im, output_mode)
# Check 0 # Check 0
im = Image.new(mode, (0, 0)) im = Image.new(input_mode, (0, 0))
for mode in modes: for output_mode in modes:
convert(im, mode) convert(im, output_mode)
def test_default(): def test_default():
@ -70,6 +70,11 @@ def test_16bit():
with Image.open("Tests/images/16bit.cropped.tif") as im: with Image.open("Tests/images/16bit.cropped.tif") as im:
_test_float_conversion(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(): def test_16bit_workaround():
with Image.open("Tests/images/16bit.cropped.tif") as im: 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") 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") im_rgb = im.convert("RGB")
assert im_rgb.info["transparency"] == (128, 128, 128) # undone assert im_rgb.info["transparency"] == (128, 128, 128) # undone
im_rgb.save(f) im_rgb.save(f)
@ -213,6 +222,20 @@ def test_p_la():
assert_image_similar(alpha, comparable, 5) assert_image_similar(alpha, comparable, 5)
def test_p2pa_alpha():
with Image.open("Tests/images/tiny.png") as im:
assert im.mode == "P"
im_pa = im.convert("PA")
assert im_pa.mode == "PA"
im_a = im_pa.getchannel("A")
for x in range(4):
alpha = 255 if x > 1 else 0
for y in range(4):
assert im_a.getpixel((x, y)) == alpha
def test_matrix_illegal_conversion(): def test_matrix_illegal_conversion():
# Arrange # Arrange
im = hopper("CMYK") im = hopper("CMYK")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ from .helper import (
assert_image_similar, assert_image_similar,
fromstring, fromstring,
hopper, hopper,
skip_unless_feature,
tostring, tostring,
) )
@ -88,6 +89,15 @@ def test_no_resize():
assert im.size == (64, 64) 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 # valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing") @pytest.mark.valgrind_known_error(reason="Known Failing")
def test_DCT_scaling_edges(): 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: with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) 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) assert_image_equal_tofile(im, expected)
def test_polygon_1px_high_translucent():
# Test drawing a translucent 1px high polygon
# Arrange
im = Image.new("RGB", (4, 3))
draw = ImageDraw.Draw(im, "RGBA")
expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png"
# Act
draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127))
# Assert
assert_image_equal_tofile(im, expected)
def test_polygon_translucent(): def test_polygon_translucent():
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -1440,3 +1454,23 @@ def test_continuous_horizontal_edges_polygon():
assert_image_equal_tofile( assert_image_equal_tofile(
img, expected, "continuous horizontal edges polygon failed" 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 import pytest
from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features from PIL import (
BmpImagePlugin,
EpsImagePlugin,
Image,
ImageFile,
UnidentifiedImageError,
_binary,
features,
)
from .helper import ( from .helper import (
assert_image, assert_image,
@ -35,9 +43,9 @@ class TestImageFile:
parser = ImageFile.Parser() parser = ImageFile.Parser()
parser.feed(data) parser.feed(data)
imOut = parser.close() im_out = parser.close()
return im, imOut return im, im_out
assert_image_equal(*roundtrip("BMP")) assert_image_equal(*roundtrip("BMP"))
im1, im2 = roundtrip("GIF") im1, im2 = roundtrip("GIF")
@ -200,6 +208,9 @@ class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer): def encode(self, buffer):
return 1, 1, b"" return 1, 1, b""
def cleanup(self):
self.cleanup_called = True
xoff, yoff, xsize, ysize = 10, 20, 100, 100 xoff, yoff, xsize, ysize = 10, 20, 100, 100
@ -327,10 +338,12 @@ class TestPyEncoder(CodecsTest):
im = MockImageFile(buf) im = MockImageFile(buf)
fp = BytesIO() fp = BytesIO()
self.encoder.cleanup_called = False
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
) )
assert self.encoder.cleanup_called
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
@ -372,3 +385,7 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None) encoder.encode_to_file(None, None)
def test_zero_height(self):
with pytest.raises(UnidentifiedImageError):
Image.open("Tests/images/zero_height.j2k")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,8 @@ def setup_module():
# setup tk # setup tk
tk.Frame() tk.Frame()
# root = tk.Tk() # root = tk.Tk()
except RuntimeError as v:
pytest.skip(f"RuntimeError: {v}")
except tk.TclError as v: except tk.TclError as v:
pytest.skip(f"TCL Error: {v}") pytest.skip(f"TCL Error: {v}")
@ -75,8 +77,16 @@ def test_photoimage_blank():
assert im_tk.width() == 100 assert im_tk.width() == 100
assert im_tk.height() == 100 assert im_tk.height() == 100
# reloaded = ImageTk.getimage(im_tk) im = Image.new(mode, (100, 100))
# assert_image_equal(reloaded, im) 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(): def test_bitmapimage():

View File

@ -1,4 +1,3 @@
import ctypes
from io import BytesIO from io import BytesIO
from PIL import Image, ImageWin 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 # see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
if is_win32(): if is_win32():
import ctypes
import ctypes.wintypes import ctypes.wintypes
class BITMAPFILEHEADER(ctypes.Structure): 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;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;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", "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( self.assert_unpack(
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) "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): def basic(mode):
imIn = original.convert(mode) im_in = original.convert(mode)
verify(imIn) verify(im_in)
w, h = imIn.size w, h = im_in.size
imOut = imIn.copy() im_out = im_in.copy()
verify(imOut) # copy verify(im_out) # copy
imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(imOut) # transform verify(im_out) # transform
filename = str(tmp_path / "temp.im") filename = str(tmp_path / "temp.im")
imIn.save(filename) im_in.save(filename)
with Image.open(filename) as imOut: with Image.open(filename) as im_out:
verify(imIn) verify(im_in)
verify(imOut) verify(im_out)
imOut = imIn.crop((0, 0, w, h)) im_out = im_in.crop((0, 0, w, h))
verify(imOut) verify(im_out)
imOut = Image.new(mode, (w, h), None) im_out = Image.new(mode, (w, h), None)
imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
verify(imIn) verify(im_in)
verify(imOut) verify(im_out)
imIn = Image.new(mode, (1, 1), 1) im_in = Image.new(mode, (1, 1), 1)
assert imIn.getpixel((0, 0)) == 1 assert im_in.getpixel((0, 0)) == 1
imIn.putpixel((0, 0), 2) im_in.putpixel((0, 0), 2)
assert imIn.getpixel((0, 0)) == 2 assert im_in.getpixel((0, 0)) == 2
if mode == "L": if mode == "L":
maximum = 255 maximum = 255
else: else:
maximum = 32767 maximum = 32767
imIn = Image.new(mode, (1, 1), 256) im_in = Image.new(mode, (1, 1), 256)
assert imIn.getpixel((0, 0)) == min(256, maximum) assert im_in.getpixel((0, 0)) == min(256, maximum)
imIn.putpixel((0, 0), 512) im_in.putpixel((0, 0), 512)
assert imIn.getpixel((0, 0)) == min(512, maximum) assert im_in.getpixel((0, 0)) == min(512, maximum)
basic("L") basic("L")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -69,7 +69,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
Constants 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 A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. (2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
@ -97,8 +97,8 @@ Deprecated Use instead
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` ``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` ``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX`` ``Image.BOX`` ``Image.Resampling.BOX``
``Image.BILINEAR`` ``Image.Resampling.BILNEAR`` ``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR`` ``Image.LINEAR`` ``Image.Resampling.BILINEAR``
``Image.HAMMING`` ``Image.Resampling.HAMMING`` ``Image.HAMMING`` ``Image.Resampling.HAMMING``
``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` ``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
``Image.CUBIC`` ``Image.Resampling.BICUBIC`` ``Image.CUBIC`` ``Image.Resampling.BICUBIC``
@ -142,6 +142,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 Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead. :mod:`~PIL.FitsImagePlugin` instead.
FreeTypeFont.getmask2 fill parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
deprecated and will be removed in Pillow 10 (2023-07-01).
PhotoImage.paste box parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. 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 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)) self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
raise OSError("Truncated DDS file") from e raise OSError("Truncated DDS file") from e
return 0, 0 return -1, 0
class DXT5Decoder(ImageFile.PyDecoder): 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)) self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
raise OSError("Truncated DDS file") from e raise OSError("Truncated DDS file") from e
return 0, 0 return -1, 0
Image.register_decoder("DXT1", DXT1Decoder) 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 have the format. Copies of the image will contain data loaded from the file, but not
the file itself, meaning that it can no longer be considered to be in the original the file itself, meaning that it can no longer be considered to be in the original
format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
internally creates a copy of the image, the ``fp`` (file pointer), along with any internally creates a copy of the image, then any methods or attributes specific to the
methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format` format will no longer be present. The ``fp`` (file pointer) attribute will no longer be
attribute will be ``None``. present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
Fully supported formats Fully supported formats
----------------------- -----------------------
.. contents::
BLP BLP
^^^ ^^^
@ -44,8 +42,9 @@ BMP
^^^ ^^^
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``, 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 or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
is not supported. 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 The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :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 Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
writes run-length encoded files in GIF87a by default, unless GIF89a features writes files in GIF87a by default, unless GIF89a features are used or GIF89a is
are used or GIF89a is already in use. already in use. Files are written with LZW encoding.
GIF files are initially read as grayscale (``L``) or palette mode (``P``) GIF files are initially read as grayscale (``L``) or palette mode (``P``)
images, but seeking to later frames in an image will change the mode to either images. Seeking to later frames in a ``P`` image will change the image to
``RGB`` or ``RGBA``, depending on whether the first frame had transparency. ``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 The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :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. it will loop forever.
**comment** **comment**
May not be present. A comment about the image. May not be present. A comment about the image. This is the last comment found
before the current frame's image.
**extension** **extension**
May not be present. Contains application specific information. May not be present. Contains application specific information.
@ -220,17 +246,14 @@ Reading local images
The GIF loader creates an image memory the same size as the GIF files *logical The GIF loader creates an image memory the same size as the GIF files *logical
screen size*, and pastes the actual pixel data (the *local image*) into this screen size*, and pastes the actual pixel data (the *local image*) into this
image. If you only want the actual pixel rectangle, you can manipulate the image. If you only want the actual pixel rectangle, you can crop the image::
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.ImageFile.ImageFile.tile`
attributes before loading the file::
im = Image.open(...) im = Image.open(...)
if im.tile[0][0] == "gif": if im.tile[0][0] == "gif":
# only read the first "local image" from this GIF file # only read the first "local image" from this GIF file
tag, (x0, y0, x1, y1), offset, extra = im.tile[0] box = im.tile[0][1]
im.size = (x1 - x0, y1 - y0) im = im.crop(box)
im.tile = [(tag, (0, 0) + im.size, offset, extra)]
ICNS ICNS
^^^^ ^^^^
@ -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: The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**quality** **quality**
The image quality, on a scale from 0 (worst) to 95 (best). The default is The image quality, on a scale from 0 (worst) to 95 (best), or the string
75. Values above 95 should be avoided; 100 disables portions of the JPEG ``keep``. The default is 75. Values above 95 should be avoided; 100 disables
compression algorithm, and results in large files with hardly any gain in portions of the JPEG compression algorithm, and results in large files with
image quality. 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** **optimize**
If present and true, indicates that the encoder should make an extra pass 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. and must be greater than the code-block size.
**irreversible** **irreversible**
If ``True``, use the lossy Irreversible Color Transformation If ``True``, use the lossy discrete waveform transformation DWT 9-7.
followed by DWT 9-7. Defaults to ``False``, which means to use the Defaults to ``False``, which uses the lossless DWT 5-3.
Reversible Color Transformation with 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** **progression**
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``, 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 for compliant 4K files, *at least one* of the dimensions must match
4096 x 2160. 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:: .. note::
To enable JPEG 2000 support, you need to build and install the OpenJPEG 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 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. ``RGB`` data.
SGI SGI
@ -1192,6 +1233,11 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
SUN
^^^
Pillow identifies and reads Sun raster files.
WAL WAL
^^^ ^^^
@ -1206,13 +1252,13 @@ this format.
By default, a Quake2 standard palette is attached to the texture. To override By default, a Quake2 standard palette is attached to the texture. To override
the palette, use the putpalette method. the palette, use the putpalette method.
WMF WMF, EMF
^^^ ^^^^^^^^
Pillow can identify WMF files. Pillow can identify WMF and EMF files.
On Windows, it can read WMF files. By default, it will load the image at 72 On Windows, it can read WMF and EMF files. By default, it will load the image
dpi. To load it at another resolution: at 72 dpi. To load it at another resolution:
.. code-block:: python .. code-block:: python
@ -1222,7 +1268,8 @@ dpi. To load it at another resolution:
im.load(dpi=144) im.load(dpi=144)
To add other read or write support, use To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler. :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler.
.. code-block:: python .. code-block:: python

View File

@ -171,20 +171,37 @@ Rolling an image
:: ::
def roll(image, delta): def roll(im, delta):
"""Roll an image sideways.""" """Roll an image sideways."""
xsize, ysize = image.size xsize, ysize = im.size
delta = delta % xsize delta = delta % xsize
if delta == 0: if delta == 0:
return image return im
part1 = image.crop((0, 0, delta, ysize)) part1 = im.crop((0, 0, delta, ysize))
part2 = image.crop((delta, 0, xsize, ysize)) part2 = im.crop((delta, 0, xsize, ysize))
image.paste(part1, (xsize - delta, 0, xsize, ysize)) im.paste(part1, (xsize - delta, 0, xsize, ysize))
image.paste(part2, (0, 0, xsize - delta, 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 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 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 tar file, you can use the :py:class:`~PIL.ContainerIO` or
:py:class:`~PIL.TarIO` modules to access it. :py:class:`~PIL.TarIO` modules to access it.
Reading from URL
^^^^^^^^^^^^^^^^
::
from PIL import Image
from urllib.request import urlopen
url = "https://python-pillow.org/images/pillow-logo.png"
img = Image.open(urlopen(url))
Reading from a tar archive Reading from a tar archive
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -123,8 +123,12 @@ The ``tile`` attribute
To be able to read the file as well as just identifying it, the ``tile`` 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 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 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, given region in the image.
covering the full 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:: 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 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. 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 1. Setup: Pillow looks for a function in the decoder or encoder registry,
back to a function named ``[decodername]_decoder`` on the internal falling back to a function named ``[codecname]_decoder`` or
core image object. That function is called with the ``args`` tuple ``[codecname]_encoder`` on the internal core image object. That function is
from the ``tile`` setup in the ``_open`` method. called with the ``args`` tuple from the ``tile``.
2. Decoding: The decoder's decode function is repeatedly called with 2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
chunks of image data. called with chunks of image data.
3. Cleanup: If the decoder has registered a cleanup function, it will 3. Cleanup: If the codec has registered a cleanup function, it will
be called at the end of the decoding process, even if there was an be called at the end of the transformation process, even if there was an
exception raised. exception raised.
Setup Setup
----- -----
The current conventions are that the decoder setup function is named The current conventions are that the codec setup function is named
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The ``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
python binding for it is named ``[decodername]_decoder`` and is setup and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
from within the ``_imaging.c`` file in the codecs section of the named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
function array. within the ``_imaging.c`` file in the codecs section of the function array.
The setup function needs to call ``PyImaging_DecoderNew`` and at the The setup function needs to call ``PyImaging_DecoderNew`` or
very least, set the ``decode`` function pointer. The fields of ``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
interest in this object are: ``encode`` function pointer. The fields of interest in this object are:
**decode** **decode**/**encode**
Function pointer to the decode function, which has access to Function pointer to the decode or encode function, which has access to
``im``, ``state``, and the buffer of data to be added to the image. ``im``, ``state``, and the buffer of data to be transformed.
**cleanup** **cleanup**
Function pointer to the cleanup function, has access to ``state``. Function pointer to the cleanup function, has access to ``state``.
@ -369,36 +373,34 @@ interest in this object are:
**state** **state**
An ImagingCodecStateInstance, will be set by Pillow. The ``context`` 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. any format specific state or options.
**pulls_fd** **pulls_fd**/**pushes_fd**
**EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1, 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 ``state->fd`` will be a pointer to the Python file like object. The codec may
decoder may use the functions in ``codec_fd.c`` to read directly use the functions in ``codec_fd.c`` to read or write directly with the file
from the file like object rather than have the data pushed through a like object rather than have the data pushed through a buffer.
buffer. Note that this implementation may be refactored until this
warning is removed.
.. versionadded:: 3.3.0 .. versionadded:: 3.3.0
Decoding Transforming
-------- ------------
The decode function is called with the target (core) image, the The decode or encode function is called with the target (core) image, the codec
decoder state structure, and a buffer of data to be decoded. state structure, and a buffer of data to be transformed.
**Experimental** -- If ``pulls_fd`` is set, then the decode function It is the codec's responsibility to pull as much data as possible out of the
is called once, with an empty buffer. It is the decoder's buffer and return the number of bytes consumed. The next call to the codec will
responsibility to decode the entire tile in that one call. The rest of include the previous unconsumed tail. The codec function will be called
this section only applies if ``pulls_fd`` is not set. multiple times as the data processed.
It is the decoder's responsibility to pull as much data as possible Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
out of the buffer and return the number of bytes consumed. The next encode function is called once, with an empty buffer. It is the codec's
call to the decoder will include the previous unconsumed tail. The responsibility to transform the entire tile in that one call. Using this will
decoder function will be called multiple times as the data is read provide a codec with more freedom, but that freedom may mean increased memory
from the file like object. usage if the entire tile is held in memory at once by the codec.
If an error occurs, set ``state->errcode`` and return -1. If an error occurs, set ``state->errcode`` and return -1.
@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
Cleanup Cleanup
------- -------
The cleanup function is called after the decoder returns a negative The cleanup function is called after the codec returns a negative
value, or if there is a read error from the file. This function should value, or if there is an error. This function should free any allocated
free any allocated memory and release any resources from external memory and release any resources from external libraries.
libraries.
.. _file-codecs-py: .. _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 the file codecs, there are three stages in the lifetime of a
Python-based file codec: 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. instantiates the class.
2. Transforming: The instance's ``decode`` method is repeatedly called with 2. Transforming: The instance's ``decode`` method is repeatedly called with
a buffer of data to be interpreted, or the ``encode`` method is repeatedly a buffer of data to be interpreted, or the ``encode`` method is repeatedly
called with the size of data to be output. 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