Merge branch 'main' into plainPPM
|
@ -25,8 +25,8 @@ install:
|
|||
- mv c:\pillow-depends-main c:\pillow-depends
|
||||
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
|
||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
||||
- ..\pillow-depends\gs9550w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH%
|
||||
- ..\pillow-depends\gs9561w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
|
@ -43,7 +43,7 @@ build_script:
|
|||
|
||||
test_script:
|
||||
- cd c:\pillow
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
|
||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# gather the coverage data
|
||||
python3 -m pip install codecov
|
||||
if [[ $MATRIX_DOCKER ]]; then
|
||||
coverage xml --ignore-errors
|
||||
python3 -m coverage xml --ignore-errors
|
||||
else
|
||||
coverage xml
|
||||
python3 -m coverage xml
|
||||
fi
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
coverage erase
|
||||
python3 -m coverage erase
|
||||
if [ $(uname) == "Darwin" ]; then
|
||||
export CPPFLAGS="-I/usr/local/miniconda/include";
|
||||
fi
|
||||
|
|
|
@ -13,13 +13,17 @@ aptget_update()
|
|||
return 1
|
||||
fi
|
||||
}
|
||||
aptget_update || aptget_update retry || aptget_update retry
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
aptget_update || aptget_update retry || aptget_update retry
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
|
||||
fi
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade wheel
|
||||
|
@ -32,24 +36,28 @@ python3 -m pip install -U pytest-cov
|
|||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
python3 -m pip install test-image-results
|
||||
python3 -m pip install numpy
|
||||
|
||||
# PyQt5 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
# arm64, ppc64le, s390x CPUs:
|
||||
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
|
||||
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
|
||||
python3 -m pip install pyqt5
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
# TODO Remove condition when NumPy supports 3.11
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0
|
||||
python3 -m pip install pyqt6
|
||||
fi
|
||||
|
||||
# webp
|
||||
pushd depends && ./install_webp.sh && popd
|
||||
|
||||
# libimagequant
|
||||
pushd depends && ./install_imagequant.sh && popd
|
||||
|
||||
# raqm
|
||||
pushd depends && ./install_raqm.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
else
|
||||
cd depends && ./install_extra_test_images.sh && cd ..
|
||||
fi
|
||||
|
||||
# webp
|
||||
pushd depends && ./install_webp.sh && popd
|
||||
|
||||
# libimagequant
|
||||
pushd depends && ./install_imagequant.sh && popd
|
||||
|
||||
# raqm
|
||||
pushd depends && ./install_raqm.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
|
@ -16,7 +16,6 @@ trim_trailing_whitespace = true
|
|||
[*.yml]
|
||||
# Two-space indentation
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
|
|
2
.github/CONTRIBUTING.md
vendored
|
@ -4,7 +4,7 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
|
|||
|
||||
## Bug fixes, feature additions, etc.
|
||||
|
||||
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
|
||||
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
|
||||
|
||||
- Fork the Pillow repository.
|
||||
- Create a branch from `main`.
|
||||
|
|
1
.github/mergify.yml
vendored
|
@ -8,6 +8,7 @@ pull_request_rules:
|
|||
- status-success=Docker Test Successful
|
||||
- status-success=Windows Test Successful
|
||||
- status-success=MinGW Test Successful
|
||||
- status-success=Cygwin Test Successful
|
||||
- status-success=continuous-integration/appveyor/pr
|
||||
actions:
|
||||
merge:
|
||||
|
|
3
.github/workflows/macos-install.sh
vendored
|
@ -15,7 +15,8 @@ python3 -m pip install pyroma
|
|||
python3 -m pip install test-image-results
|
||||
|
||||
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
|
||||
python3 -m pip install numpy
|
||||
# TODO Remove condition when NumPy supports 3.11
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
27
.github/workflows/stale.yml
vendored
Normal file
|
@ -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
|
@ -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
|
15
.github/workflows/test-docker.yml
vendored
|
@ -11,9 +11,9 @@ jobs:
|
|||
matrix:
|
||||
docker: [
|
||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||
ubuntu-20.04-focal-arm64v8,
|
||||
ubuntu-20.04-focal-ppc64le,
|
||||
ubuntu-20.04-focal-s390x,
|
||||
ubuntu-22.04-jammy-arm64v8,
|
||||
ubuntu-22.04-jammy-ppc64le,
|
||||
ubuntu-22.04-jammy-s390x,
|
||||
# Then run the remainder
|
||||
alpine,
|
||||
amazon-2-amd64,
|
||||
|
@ -23,19 +23,20 @@ jobs:
|
|||
centos-stream-9-amd64,
|
||||
debian-10-buster-x86,
|
||||
debian-11-bullseye-x86,
|
||||
fedora-34-amd64,
|
||||
fedora-35-amd64,
|
||||
fedora-36-amd64,
|
||||
gentoo,
|
||||
ubuntu-18.04-bionic-amd64,
|
||||
ubuntu-20.04-focal-amd64,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-20.04-focal-arm64v8"
|
||||
- docker: "ubuntu-22.04-jammy-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
- docker: "ubuntu-20.04-focal-ppc64le"
|
||||
- docker: "ubuntu-22.04-jammy-ppc64le"
|
||||
qemu-arch: "ppc64le"
|
||||
- docker: "ubuntu-20.04-focal-s390x"
|
||||
- docker: "ubuntu-22.04-jammy-s390x"
|
||||
qemu-arch: "s390x"
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
|
12
.github/workflows/test-windows.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
|
||||
architecture: ["x86", "x64"]
|
||||
include:
|
||||
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
|
||||
|
@ -41,10 +41,10 @@ jobs:
|
|||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
- name: Print build system information
|
||||
run: python .github/workflows/system-info.py
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
- name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
|
@ -52,8 +52,8 @@ jobs:
|
|||
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
|
||||
|
||||
winbuild\depends\gs9550w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH
|
||||
winbuild\depends\gs9561w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
|
||||
|
||||
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
|
||||
|
||||
|
|
5
.github/workflows/test.yml
vendored
|
@ -15,6 +15,7 @@ jobs:
|
|||
python-version: [
|
||||
"pypy-3.8",
|
||||
"pypy-3.7",
|
||||
"3.11-dev",
|
||||
"3.10",
|
||||
"3.9",
|
||||
"3.8",
|
||||
|
@ -59,6 +60,8 @@ jobs:
|
|||
if: startsWith(matrix.os, 'macOS')
|
||||
run: |
|
||||
.github/workflows/macos-install.sh
|
||||
env:
|
||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
@ -93,7 +96,7 @@ jobs:
|
|||
- name: Docs
|
||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
|
||||
run: |
|
||||
python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
|
||||
python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
|
||||
make doccheck
|
||||
|
||||
- name: After success
|
||||
|
|
2
.github/workflows/tidelift.yml
vendored
|
@ -4,9 +4,11 @@ on:
|
|||
- cron: "30 2 * * *" # daily at 02:30 UTC
|
||||
push:
|
||||
paths:
|
||||
- "Pipfile*"
|
||||
- ".github/workflows/tidelift.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "Pipfile*"
|
||||
- ".github/workflows/tidelift.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: fc0be6eb1e2a96091e6f64009ee5e9081bf8b6c6 # frozen: 22.1.0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py37"]
|
||||
|
@ -9,38 +9,43 @@ repos:
|
|||
types: []
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: ca52c4245639abd55c970e6bbbca95cab3de22d8 # frozen: v1.1.13
|
||||
rev: v1.2.0
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.6
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
autoupdate_schedule: monthly
|
||||
|
|
2637
CHANGES.rst
4
Makefile
|
@ -77,7 +77,7 @@ release-test:
|
|||
-rm dist/*.egg
|
||||
-rmdir dist
|
||||
python3 -m pytest -qq
|
||||
python3 -m check-manifest
|
||||
python3 -m check_manifest
|
||||
python3 -m pyroma .
|
||||
$(MAKE) readme
|
||||
|
||||
|
@ -85,6 +85,8 @@ release-test:
|
|||
sdist:
|
||||
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
|
||||
python3 -m build --sdist
|
||||
python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
|
||||
python3 -m twine check --strict dist/*
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
|
|
|
@ -36,6 +36,9 @@ As of 2019, Pillow development is
|
|||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
|
||||
alt="GitHub Actions build status (Test MinGW)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
|
||||
alt="GitHub Actions build status (Test Cygwin)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
|
||||
alt="GitHub Actions build status (Test Docker)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
|
||||
|
|
11
RELEASING.md
|
@ -24,13 +24,12 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.0*
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.0*
|
||||
```
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
|
||||
|
@ -61,13 +60,12 @@ Released as needed for security, installation or critical bug fixes.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.1*
|
||||
python3 -m twine check --strict dist/*
|
||||
python3 -m twine upload dist/Pillow-5.2.1*
|
||||
```
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
|
||||
|
@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
|
|
|
@ -8,7 +8,7 @@ Dependencies
|
|||
|
||||
Install::
|
||||
|
||||
python3 -m pip install pytest pytest-cov
|
||||
python3 -m pip install pytest pytest-cov pytest-timeout
|
||||
|
||||
Execution
|
||||
---------
|
||||
|
|
|
@ -324,7 +324,7 @@ def is_mingw():
|
|||
return sysconfig.get_platform() == "mingw"
|
||||
|
||||
|
||||
class cached_property:
|
||||
class CachedProperty:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
|
|
BIN
Tests/images/16bit.r.tif
Normal file
BIN
Tests/images/comment_after_last_frame.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/cross_scan_line_truncated.tga
Normal file
Before Width: | Height: | Size: 58 B After Width: | Height: | Size: 198 B |
BIN
Tests/images/duplicate_number_of_loops.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/hopper_bigtiff.tif
Normal file
BIN
Tests/images/hopper_rle8.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/hopper_rle8_row_overflow.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/imagedraw/discontiguous_corners_polygon.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
Tests/images/imagedraw_polygon_1px_high_translucent.png
Normal file
After Width: | Height: | Size: 76 B |
BIN
Tests/images/issue_6194.j2k
Normal file
BIN
Tests/images/multiple_comments.gif
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Tests/images/no_palette.gif
Normal file
After Width: | Height: | Size: 48 B |
BIN
Tests/images/no_palette_with_background.gif
Normal file
After Width: | Height: | Size: 54 B |
BIN
Tests/images/no_palette_with_transparency.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/second_frame_comment.gif
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
Tests/images/tiff_wrong_bits_per_sample_3.tiff
Normal file
BIN
Tests/images/tiny.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
Tests/images/zero_height.j2k
Normal file
|
@ -40,6 +40,7 @@ def test_questionable():
|
|||
"rgb32fakealpha.bmp",
|
||||
"rgb24largepal.bmp",
|
||||
"pal8os2sp.bmp",
|
||||
"pal8rletrns.bmp",
|
||||
"rgb32bf-xbgr.bmp",
|
||||
]
|
||||
for f in get_files("q"):
|
||||
|
|
|
@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1):
|
|||
return image._new(image.im.box_blur(radius, n))
|
||||
|
||||
|
||||
def assertImage(im, data, delta=0):
|
||||
def assert_image(im, data, delta=0):
|
||||
it = iter(im.getdata())
|
||||
for data_row in data:
|
||||
im_row = [next(it) for _ in range(im.size[0])]
|
||||
|
@ -35,12 +35,12 @@ def assertImage(im, data, delta=0):
|
|||
next(it)
|
||||
|
||||
|
||||
def assertBlur(im, radius, data, passes=1, delta=0):
|
||||
def assert_blur(im, radius, data, passes=1, delta=0):
|
||||
# check grayscale image
|
||||
assertImage(box_blur(im, radius, passes), data, delta)
|
||||
assert_image(box_blur(im, radius, passes), data, delta)
|
||||
rgba = Image.merge("RGBA", (im, im, im, im))
|
||||
for band in box_blur(rgba, radius, passes).split():
|
||||
assertImage(band, data, delta)
|
||||
assert_image(band, data, delta)
|
||||
|
||||
|
||||
def test_color_modes():
|
||||
|
@ -64,7 +64,7 @@ def test_color_modes():
|
|||
|
||||
|
||||
def test_radius_0():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0,
|
||||
[
|
||||
|
@ -80,7 +80,7 @@ def test_radius_0():
|
|||
|
||||
|
||||
def test_radius_0_02():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.02,
|
||||
[
|
||||
|
@ -97,7 +97,7 @@ def test_radius_0_02():
|
|||
|
||||
|
||||
def test_radius_0_05():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.05,
|
||||
[
|
||||
|
@ -114,7 +114,7 @@ def test_radius_0_05():
|
|||
|
||||
|
||||
def test_radius_0_1():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.1,
|
||||
[
|
||||
|
@ -131,7 +131,7 @@ def test_radius_0_1():
|
|||
|
||||
|
||||
def test_radius_0_5():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.5,
|
||||
[
|
||||
|
@ -148,7 +148,7 @@ def test_radius_0_5():
|
|||
|
||||
|
||||
def test_radius_1():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
@ -165,7 +165,7 @@ def test_radius_1():
|
|||
|
||||
|
||||
def test_radius_1_5():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1.5,
|
||||
[
|
||||
|
@ -182,7 +182,7 @@ def test_radius_1_5():
|
|||
|
||||
|
||||
def test_radius_bigger_then_half():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
3,
|
||||
[
|
||||
|
@ -199,7 +199,7 @@ def test_radius_bigger_then_half():
|
|||
|
||||
|
||||
def test_radius_bigger_then_width():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
10,
|
||||
[
|
||||
|
@ -214,7 +214,7 @@ def test_radius_bigger_then_width():
|
|||
|
||||
|
||||
def test_extreme_large_radius():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
600,
|
||||
[
|
||||
|
@ -229,7 +229,7 @@ def test_extreme_large_radius():
|
|||
|
||||
|
||||
def test_two_passes():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
@ -247,7 +247,7 @@ def test_two_passes():
|
|||
|
||||
|
||||
def test_three_passes():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
|
|
@ -15,27 +15,27 @@ except ImportError:
|
|||
class TestColorLut3DCoreAPI:
|
||||
def generate_identity_table(self, channels, size):
|
||||
if isinstance(size, tuple):
|
||||
size1D, size2D, size3D = size
|
||||
size_1d, size_2d, size_3d = size
|
||||
else:
|
||||
size1D, size2D, size3D = (size, size, size)
|
||||
size_1d, size_2d, size_3d = (size, size, size)
|
||||
|
||||
table = [
|
||||
[
|
||||
r / (size1D - 1) if size1D != 1 else 0,
|
||||
g / (size2D - 1) if size2D != 1 else 0,
|
||||
b / (size3D - 1) if size3D != 1 else 0,
|
||||
r / (size1D - 1) if size1D != 1 else 0,
|
||||
g / (size2D - 1) if size2D != 1 else 0,
|
||||
r / (size_1d - 1) if size_1d != 1 else 0,
|
||||
g / (size_2d - 1) if size_2d != 1 else 0,
|
||||
b / (size_3d - 1) if size_3d != 1 else 0,
|
||||
r / (size_1d - 1) if size_1d != 1 else 0,
|
||||
g / (size_2d - 1) if size_2d != 1 else 0,
|
||||
][:channels]
|
||||
for b in range(size3D)
|
||||
for g in range(size2D)
|
||||
for r in range(size1D)
|
||||
for b in range(size_3d)
|
||||
for g in range(size_2d)
|
||||
for r in range(size_1d)
|
||||
]
|
||||
return (
|
||||
channels,
|
||||
size1D,
|
||||
size2D,
|
||||
size3D,
|
||||
size_1d,
|
||||
size_2d,
|
||||
size_3d,
|
||||
[item for sublist in table for item in sublist],
|
||||
)
|
||||
|
||||
|
@ -567,7 +567,7 @@ class TestTransformColorLut3D:
|
|||
assert tuple(lut.size) == tuple(source.size)
|
||||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
assert lut.table[0:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
|
||||
assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
|
||||
|
||||
def test_3_to_4_channels(self):
|
||||
source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
|
||||
|
@ -576,7 +576,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) != len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:16] == [
|
||||
assert lut.table[:16] == [
|
||||
0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1,
|
||||
0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]
|
||||
# fmt: on
|
||||
|
@ -592,7 +592,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) != len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:18] == [
|
||||
assert lut.table[:18] == [
|
||||
1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0,
|
||||
1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]
|
||||
# fmt: on
|
||||
|
@ -606,7 +606,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:16] == [
|
||||
assert lut.table[:16] == [
|
||||
0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5,
|
||||
0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]
|
||||
# fmt: on
|
||||
|
@ -622,7 +622,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:18] == [
|
||||
assert lut.table[:18] == [
|
||||
0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0,
|
||||
0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]
|
||||
# fmt: on
|
||||
|
@ -639,7 +639,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:16] == [
|
||||
assert lut.table[:16] == [
|
||||
0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5,
|
||||
0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5]
|
||||
# fmt: on
|
||||
|
|
|
@ -51,7 +51,6 @@ class TestDecompressionBomb:
|
|||
with Image.open(TEST_FILE):
|
||||
pass
|
||||
|
||||
@pytest.mark.xfail(reason="different exception")
|
||||
def test_exception_ico(self):
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
with Image.open("Tests/images/decompression_bomb.ico"):
|
||||
|
@ -70,15 +69,15 @@ class TestDecompressionBomb:
|
|||
|
||||
class TestDecompressionCrop:
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
def setup_class(cls):
|
||||
width, height = 128, 128
|
||||
Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
|
||||
|
||||
@classmethod
|
||||
def teardown_class(self):
|
||||
def teardown_class(cls):
|
||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||
|
||||
def testEnlargeCrop(self):
|
||||
def test_enlarge_crop(self):
|
||||
# Crops can extend the extents, therefore we should have the
|
||||
# same decompression bomb warnings on them.
|
||||
with hopper() as src:
|
||||
|
|
91
Tests/test_deprecate.py
Normal file
|
@ -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)
|
18
Tests/test_deprecated_imageqt.py
Normal 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
|
|
@ -637,6 +637,15 @@ def test_apng_save_blend(tmp_path):
|
|||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open("Tests/images/apng/delay.png")
|
||||
im.seek(1)
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(0)
|
||||
|
||||
|
||||
def test_constants_deprecation():
|
||||
for enum, prefix in {
|
||||
PngImagePlugin.Disposal: "APNG_DISPOSE_",
|
||||
|
|
|
@ -4,7 +4,12 @@ import pytest
|
|||
|
||||
from PIL import BmpImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_equal_tofile,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
)
|
||||
|
||||
|
||||
def test_sanity(tmp_path):
|
||||
|
@ -125,6 +130,42 @@ def test_rgba_bitfields():
|
|||
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
||||
|
||||
|
||||
def test_rle8():
|
||||
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||
|
||||
# This test image has been manually hexedited
|
||||
# to have rows with too much data
|
||||
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||
|
||||
# Signal end of bitmap before the image is finished
|
||||
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
|
||||
data = fp.read(1063) + b"\x01"
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_name,length",
|
||||
(
|
||||
# EOF immediately after the header
|
||||
("Tests/images/hopper_rle8.bmp", 1078),
|
||||
# EOF during delta
|
||||
("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
|
||||
# EOF when reading data in absolute mode
|
||||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||
),
|
||||
)
|
||||
def test_rle8_eof(file_name, length):
|
||||
with open(file_name, "rb") as fp:
|
||||
data = fp.read(length)
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_offset():
|
||||
# This image has been hexedited
|
||||
# to exclude the palette size from the pixel data offset
|
||||
|
|
|
@ -46,6 +46,15 @@ def test_closed_file():
|
|||
im.close()
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open(animated_test_file)
|
||||
im.seek(1)
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(0)
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(static_test_file) as im:
|
||||
|
|
|
@ -3,7 +3,7 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features
|
||||
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -46,6 +46,19 @@ def test_closed_file():
|
|||
im.close()
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open("Tests/images/iss634.gif")
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.is_animated
|
||||
with pytest.raises(ValueError):
|
||||
im.n_frames
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(1)
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(TEST_GIF) as im:
|
||||
|
@ -59,6 +72,51 @@ def test_invalid_file():
|
|||
GifImagePlugin.GifImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_l_mode_transparency():
|
||||
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
assert im.info["transparency"] == 255
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
|
||||
|
||||
def test_strategy():
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
expected_zero = im.convert("RGB")
|
||||
|
||||
im.seek(1)
|
||||
expected_one = im.convert("RGB")
|
||||
|
||||
try:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "RGB"
|
||||
assert_image_equal(im, expected_zero)
|
||||
|
||||
GifImagePlugin.LOADING_STRATEGY = (
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
)
|
||||
# Stay in P mode with only a global palette
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "P"
|
||||
assert_image_equal(im.convert("RGB"), expected_one)
|
||||
|
||||
# Change to RGB mode when a frame has an individual palette
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
|
||||
def test_optimize():
|
||||
def test_grayscale(optimize):
|
||||
im = Image.new("L", (1, 1), 0)
|
||||
|
@ -296,16 +354,23 @@ def test_seek_rewind():
|
|||
assert_image_equal(im, expected)
|
||||
|
||||
|
||||
def test_n_frames():
|
||||
for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]:
|
||||
# Test is_animated before n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
@pytest.mark.parametrize(
|
||||
"path, n_frames",
|
||||
(
|
||||
(TEST_GIF, 1),
|
||||
("Tests/images/comment_after_last_frame.gif", 2),
|
||||
("Tests/images/iss634.gif", 42),
|
||||
),
|
||||
)
|
||||
def test_n_frames(path, n_frames):
|
||||
# Test is_animated before n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
# Test is_animated after n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
# Test is_animated after n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
|
||||
def test_no_change():
|
||||
|
@ -383,18 +448,38 @@ def test_dispose_background_transparency():
|
|||
assert px[35, 30][3] == 0
|
||||
|
||||
|
||||
def test_transparent_dispose():
|
||||
expected_colors = [
|
||||
(2, 1, 2),
|
||||
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
|
||||
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
|
||||
]
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
for frame in range(3):
|
||||
img.seek(frame)
|
||||
for x in range(3):
|
||||
color = img.getpixel((x, 0))
|
||||
assert color == expected_colors[frame][x]
|
||||
@pytest.mark.parametrize(
|
||||
"loading_strategy, expected_colors",
|
||||
(
|
||||
(
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
|
||||
(
|
||||
(2, 1, 2),
|
||||
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
|
||||
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
|
||||
),
|
||||
),
|
||||
(
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
|
||||
(
|
||||
(2, 1, 2),
|
||||
(0, 1, 0),
|
||||
(2, 1, 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_transparent_dispose(loading_strategy, expected_colors):
|
||||
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||
try:
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
for frame in range(3):
|
||||
img.seek(frame)
|
||||
for x in range(3):
|
||||
color = img.getpixel((x, 0))
|
||||
assert color == expected_colors[frame][x]
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
|
||||
def test_dispose_previous():
|
||||
|
@ -554,7 +639,8 @@ def test_dispose2_background(tmp_path):
|
|||
assert im.getpixel((0, 0)) == (255, 0, 0)
|
||||
|
||||
|
||||
def test_transparency_in_second_frame():
|
||||
def test_transparency_in_second_frame(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||
assert im.info["transparency"] == 0
|
||||
|
||||
|
@ -564,6 +650,14 @@ def test_transparency_in_second_frame():
|
|||
|
||||
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
|
||||
|
||||
im.save(out, save_all=True)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
reread.seek(reread.tell() + 1)
|
||||
assert_image_equal_tofile(
|
||||
reread, "Tests/images/different_transparency_merged.png"
|
||||
)
|
||||
|
||||
|
||||
def test_no_transparency_in_second_frame():
|
||||
with Image.open("Tests/images/iss634.gif") as img:
|
||||
|
@ -575,6 +669,22 @@ def test_no_transparency_in_second_frame():
|
|||
assert img.histogram()[255] == 0
|
||||
|
||||
|
||||
def test_remapped_transparency(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = Image.new("P", (1, 2))
|
||||
im2 = im.copy()
|
||||
|
||||
# Add transparency at a higher index
|
||||
# so that it will be optimized to a lower index
|
||||
im.putpixel((0, 1), 5)
|
||||
im.info["transparency"] = 5
|
||||
im.save(out, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
|
||||
|
||||
|
||||
def test_duration(tmp_path):
|
||||
duration = 1000
|
||||
|
||||
|
@ -626,6 +736,23 @@ def test_multiple_duration(tmp_path):
|
|||
pass
|
||||
|
||||
|
||||
def test_roundtrip_info_duration(tmp_path):
|
||||
duration_list = [100, 500, 500]
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as im:
|
||||
assert [
|
||||
frame.info["duration"] for frame in ImageSequence.Iterator(im)
|
||||
] == duration_list
|
||||
|
||||
im.save(out, save_all=True)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert [
|
||||
frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
|
||||
] == duration_list
|
||||
|
||||
|
||||
def test_identical_frames(tmp_path):
|
||||
duration_list = [1000, 1500, 2000, 4000]
|
||||
|
||||
|
@ -677,9 +804,16 @@ def test_number_of_loops(tmp_path):
|
|||
im = Image.new("L", (100, 100), "#000")
|
||||
im.save(out, loop=number_of_loops)
|
||||
with Image.open(out) as reread:
|
||||
|
||||
assert reread.info["loop"] == number_of_loops
|
||||
|
||||
# Check that even if a subsequent GIF frame has the number of loops specified,
|
||||
# only the value from the first frame is used
|
||||
with Image.open("Tests/images/duplicate_number_of_loops.gif") as im:
|
||||
assert im.info["loop"] == 2
|
||||
|
||||
im.seek(1)
|
||||
assert im.info["loop"] == 2
|
||||
|
||||
|
||||
def test_background(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
@ -712,6 +846,9 @@ def test_comment(tmp_path):
|
|||
with Image.open(out) as reread:
|
||||
assert reread.info["comment"] == im.info["comment"].encode()
|
||||
|
||||
# Test that GIF89a is used for comments
|
||||
assert reread.info["version"] == b"GIF89a"
|
||||
|
||||
|
||||
def test_comment_over_255(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
@ -722,43 +859,95 @@ def test_comment_over_255(tmp_path):
|
|||
im.info["comment"] = comment
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
|
||||
assert reread.info["comment"] == comment
|
||||
|
||||
# Test that GIF89a is used for comments
|
||||
assert reread.info["version"] == b"GIF89a"
|
||||
|
||||
|
||||
def test_zero_comment_subblocks():
|
||||
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
|
||||
assert_image_equal_tofile(im, TEST_GIF)
|
||||
|
||||
|
||||
def test_read_multiple_comment_blocks():
|
||||
with Image.open("Tests/images/multiple_comments.gif") as im:
|
||||
# Multiple comment blocks in a frame are separated not concatenated
|
||||
assert im.info["comment"] == b"Test comment 1\nTest comment 2"
|
||||
|
||||
|
||||
def test_empty_string_comment(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert "comment" in im.info
|
||||
|
||||
# Empty string comment should suppress existing comment
|
||||
im.save(out, save_all=True, comment="")
|
||||
|
||||
with Image.open(out) as reread:
|
||||
for frame in ImageSequence.Iterator(reread):
|
||||
assert "comment" not in frame.info
|
||||
|
||||
|
||||
def test_retain_comment_in_subsequent_frames(tmp_path):
|
||||
# Test that a comment block at the beginning is kept
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
assert frame.info["comment"] == b"Created with GIMP"
|
||||
|
||||
with Image.open("Tests/images/second_frame_comment.gif") as im:
|
||||
assert "comment" not in im.info
|
||||
|
||||
# Test that a comment in the middle is read
|
||||
im.seek(1)
|
||||
assert im.info["comment"] == b"Comment in the second frame"
|
||||
|
||||
# Test that it is still present in a later frame
|
||||
im.seek(2)
|
||||
assert im.info["comment"] == b"Comment in the second frame"
|
||||
|
||||
# Test that rewinding removes the comment
|
||||
im.seek(0)
|
||||
assert "comment" not in im.info
|
||||
|
||||
# Test that a saved image keeps the comment
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/dispose_prev.gif") as im:
|
||||
im.save(out, save_all=True, comment="Test")
|
||||
|
||||
with Image.open(out) as reread:
|
||||
for frame in ImageSequence.Iterator(reread):
|
||||
assert frame.info["comment"] == b"Test"
|
||||
|
||||
|
||||
def test_version(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
def assertVersionAfterSave(im, version):
|
||||
def assert_version_after_save(im, version):
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["version"] == version
|
||||
|
||||
# Test that GIF87a is used by default
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
# Test setting the version to 89a
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
im.info["version"] = b"89a"
|
||||
assertVersionAfterSave(im, b"GIF89a")
|
||||
assert_version_after_save(im, b"GIF89a")
|
||||
|
||||
# Test that adding a GIF89a feature changes the version
|
||||
im.info["transparency"] = 1
|
||||
assertVersionAfterSave(im, b"GIF89a")
|
||||
assert_version_after_save(im, b"GIF89a")
|
||||
|
||||
# Test that a GIF87a image is also saved in that format
|
||||
with Image.open("Tests/images/test.colors.gif") as im:
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
# Test that a GIF89a image is also saved in that format
|
||||
im.info["version"] = b"GIF89a"
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
|
||||
def test_append_images(tmp_path):
|
||||
|
@ -773,10 +962,10 @@ def test_append_images(tmp_path):
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
im.save(out, save_all=True, append_images=imGenerator(ims))
|
||||
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert reread.n_frames == 3
|
||||
|
@ -831,6 +1020,17 @@ def test_rgb_transparency(tmp_path):
|
|||
assert "transparency" not in reloaded.info
|
||||
|
||||
|
||||
def test_rgba_transparency(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = hopper("P")
|
||||
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded.seek(1)
|
||||
assert_image_equal(hopper("P").convert("RGB"), reloaded)
|
||||
|
||||
|
||||
def test_bbox(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
|
@ -960,6 +1160,11 @@ def test_lzw_bits():
|
|||
def test_extents():
|
||||
with Image.open("Tests/images/test_extents.gif") as im:
|
||||
assert im.size == (100, 100)
|
||||
|
||||
# Check that n_frames does not change the size
|
||||
assert im.n_frames == 2
|
||||
assert im.size == (100, 100)
|
||||
|
||||
im.seek(1)
|
||||
assert im.size == (150, 150)
|
||||
|
||||
|
|
|
@ -4,15 +4,13 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import IcnsImagePlugin, Image, _binary, features
|
||||
from PIL import IcnsImagePlugin, Image, _binary
|
||||
|
||||
from .helper import assert_image_equal, assert_image_similar_tofile
|
||||
from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless_feature
|
||||
|
||||
# sample icon file
|
||||
TEST_FILE = "Tests/images/pillow.icns"
|
||||
|
||||
ENABLE_JPEG2K = features.check_codec("jpg_2000")
|
||||
|
||||
|
||||
def test_sanity():
|
||||
# Loading this icon by default should result in the largest size
|
||||
|
@ -111,14 +109,12 @@ def test_older_icon():
|
|||
assert im2.size == (wr, hr)
|
||||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
def test_jp2_icon():
|
||||
# This icon uses JPEG 2000 images instead of the PNG images.
|
||||
# The advantage of doing this is that OS X 10.5 supports JPEG 2000
|
||||
# but not PNG; some commercial software therefore does just this.
|
||||
|
||||
if not ENABLE_JPEG2K:
|
||||
return
|
||||
|
||||
with Image.open("Tests/images/pillow3.icns") as im:
|
||||
for w, h, r in im.info["sizes"]:
|
||||
wr = w * r
|
||||
|
@ -149,6 +145,7 @@ def test_not_an_icns_file():
|
|||
IcnsImagePlugin.IcnsFile(fp)
|
||||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
def test_icns_decompression_bomb():
|
||||
with Image.open(
|
||||
"Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import io
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -70,6 +71,53 @@ def test_save_to_bytes():
|
|||
)
|
||||
|
||||
|
||||
def test_no_duplicates(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
|
||||
im = hopper()
|
||||
sizes = [(32, 32), (64, 64)]
|
||||
im.save(temp_file, "ico", sizes=sizes)
|
||||
|
||||
sizes.append(sizes[-1])
|
||||
im.save(temp_file2, "ico", sizes=sizes)
|
||||
|
||||
assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
|
||||
|
||||
|
||||
def test_different_bit_depths(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
|
||||
im = hopper()
|
||||
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
|
||||
hopper("1").save(
|
||||
temp_file2,
|
||||
"ico",
|
||||
bitmap_format="bmp",
|
||||
sizes=[(128, 128)],
|
||||
append_images=[im],
|
||||
)
|
||||
|
||||
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
|
||||
|
||||
# Test that only matching sizes of different bit depths are saved
|
||||
temp_file3 = str(tmp_path / "temp3.ico")
|
||||
temp_file4 = str(tmp_path / "temp4.ico")
|
||||
|
||||
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
im.save(
|
||||
temp_file4,
|
||||
"ico",
|
||||
bitmap_format="bmp",
|
||||
sizes=[(128, 128)],
|
||||
append_images=[Image.new("P", (64, 64))],
|
||||
)
|
||||
|
||||
assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
||||
def test_save_to_bytes_bmp(mode):
|
||||
output = io.BytesIO()
|
||||
|
|
|
@ -68,6 +68,13 @@ class TestFileJpeg:
|
|||
assert im.format == "JPEG"
|
||||
assert im.get_format_mimetype() == "image/jpeg"
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero(self, size, tmp_path):
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
im = Image.new("RGB", size)
|
||||
with pytest.raises(ValueError):
|
||||
im.save(f)
|
||||
|
||||
def test_app(self):
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
@ -736,7 +743,7 @@ class TestFileJpeg:
|
|||
|
||||
# Act / Assert
|
||||
# "When the image resolution is unknown, 72 [dpi] is designated."
|
||||
# http://www.exiv2.org/tags.html
|
||||
# https://exiv2.org/tags.html
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_invalid_exif(self):
|
||||
|
|
|
@ -209,6 +209,49 @@ def test_layers():
|
|||
assert_image_similar(im, test_card, 0.4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name, args, offset, data",
|
||||
(
|
||||
("foo.j2k", {}, 0, b"\xff\x4f"),
|
||||
("foo.jp2", {}, 4, b"jP"),
|
||||
(None, {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||
("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
|
||||
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
|
||||
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
|
||||
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
|
||||
),
|
||||
)
|
||||
def test_no_jp2(name, args, offset, data):
|
||||
out = BytesIO()
|
||||
if name:
|
||||
out.name = name
|
||||
test_card.save(out, "JPEG2000", **args)
|
||||
out.seek(offset)
|
||||
assert out.read(2) == data
|
||||
|
||||
|
||||
def test_mct():
|
||||
# Three component
|
||||
for val in (0, 1):
|
||||
out = BytesIO()
|
||||
test_card.save(out, "JPEG2000", mct=val, no_jp2=True)
|
||||
|
||||
assert out.getvalue()[59] == val
|
||||
with Image.open(out) as im:
|
||||
assert_image_similar(im, test_card, 1.0e-3)
|
||||
|
||||
# Single component should have MCT disabled
|
||||
for val in (0, 1):
|
||||
out = BytesIO()
|
||||
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
|
||||
jp2.save(out, "JPEG2000", mct=val, no_jp2=True)
|
||||
|
||||
assert out.getvalue()[53] == 0
|
||||
with Image.open(out) as im:
|
||||
assert_image_similar(im, jp2, 1.0e-3)
|
||||
|
||||
|
||||
def test_rgba():
|
||||
# Arrange
|
||||
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
|
||||
|
@ -255,6 +298,11 @@ def test_16bit_jp2_roundtrips():
|
|||
assert_image_equal(im, jp2)
|
||||
|
||||
|
||||
def test_issue_6194():
|
||||
with Image.open("Tests/images/issue_6194.j2k") as im:
|
||||
assert im.getpixel((5, 5)) == 31
|
||||
|
||||
|
||||
def test_unbound_local():
|
||||
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -4,7 +4,6 @@ import itertools
|
|||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from ctypes import c_float
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
val = original[tag]
|
||||
if tag.endswith("Resolution"):
|
||||
if legacy_api:
|
||||
assert (
|
||||
c_float(val[0][0] / val[0][1]).value
|
||||
== c_float(value[0][0] / value[0][1]).value
|
||||
assert val[0][0] / val[0][1] == (
|
||||
4294967295 / 113653537
|
||||
), f"{tag} didn't roundtrip"
|
||||
else:
|
||||
assert (
|
||||
c_float(val).value == c_float(value).value
|
||||
), f"{tag} didn't roundtrip"
|
||||
assert val == 37.79000115940079, f"{tag} didn't roundtrip"
|
||||
else:
|
||||
assert val == value, f"{tag} didn't roundtrip"
|
||||
|
||||
|
@ -501,8 +497,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, compression="tiff_adobe_deflate")
|
||||
assert_image_equal_tofile(im, out)
|
||||
|
||||
def test_palette_save(self, tmp_path):
|
||||
im = hopper("P")
|
||||
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||
def test_palette_save(self, im, tmp_path):
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
|
|
|
@ -48,6 +48,14 @@ def test_closed_file():
|
|||
im.close()
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open(test_files[0])
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(1)
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(test_files[0]) as im:
|
||||
|
@ -116,6 +124,15 @@ def test_parallax():
|
|||
assert exif.get_ifd(0x927C)[0xB211] == -3.125
|
||||
|
||||
|
||||
def test_reload_exif_after_seek():
|
||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||
exif = im.getexif()
|
||||
del exif[296]
|
||||
|
||||
im.seek(1)
|
||||
assert 296 in exif
|
||||
|
||||
|
||||
def test_mp():
|
||||
for test_file in test_files:
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -145,10 +162,10 @@ def test_mp_attribute():
|
|||
for test_file in test_files:
|
||||
with Image.open(test_file) as im:
|
||||
mpinfo = im._getmp()
|
||||
frameNumber = 0
|
||||
frame_number = 0
|
||||
for mpentry in mpinfo[0xB002]:
|
||||
mpattr = mpentry["Attribute"]
|
||||
if frameNumber:
|
||||
if frame_number:
|
||||
assert not mpattr["RepresentativeImageFlag"]
|
||||
else:
|
||||
assert mpattr["RepresentativeImageFlag"]
|
||||
|
@ -157,7 +174,7 @@ def test_mp_attribute():
|
|||
assert mpattr["ImageDataFormat"] == "JPEG"
|
||||
assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
|
||||
assert mpattr["Reserved"] == 0
|
||||
frameNumber += 1
|
||||
frame_number += 1
|
||||
|
||||
|
||||
def test_seek():
|
||||
|
|
|
@ -131,10 +131,10 @@ def test_save_all(tmp_path):
|
|||
assert os.path.getsize(outfile) > 0
|
||||
|
||||
# Test appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
im.save(outfile, save_all=True, append_images=imGenerator(ims))
|
||||
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
||||
|
||||
assert os.path.isfile(outfile)
|
||||
assert os.path.getsize(outfile) > 0
|
||||
|
@ -253,9 +253,9 @@ def test_pdf_append(tmp_path):
|
|||
check_pdf_pages_consistency(pdf)
|
||||
|
||||
# append two images
|
||||
mode_CMYK = hopper("CMYK")
|
||||
mode_P = hopper("P")
|
||||
mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P])
|
||||
mode_cmyk = hopper("CMYK")
|
||||
mode_p = hopper("P")
|
||||
mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p])
|
||||
|
||||
# open the PDF again, check pages and info again
|
||||
with PdfParser.PdfParser(pdf_filename) as pdf:
|
||||
|
|
|
@ -635,6 +635,17 @@ class TestFilePng:
|
|||
|
||||
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
|
||||
|
||||
@pytest.mark.parametrize("cid", (b"IHDR", b"pHYs", b"acTL", b"fcTL", b"fdAT"))
|
||||
def test_truncated_chunks(self, cid):
|
||||
fp = BytesIO()
|
||||
with PngImagePlugin.PngStream(fp) as png:
|
||||
with pytest.raises(ValueError):
|
||||
png.call(cid, 0, 0)
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
png.call(cid, 0, 0)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
def test_specify_bits(self, tmp_path):
|
||||
im = hopper("P")
|
||||
|
||||
|
|
|
@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
|
|||
|
||||
def test_sanity():
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.load()
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (128, 128)
|
||||
assert im.format, "PPM"
|
||||
assert im.format == "PPM"
|
||||
assert im.get_format_mimetype() == "image/x-portable-pixmap"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data, mode, pixels",
|
||||
(
|
||||
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
||||
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
||||
# P6 with maxval < 255
|
||||
(
|
||||
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
|
||||
"RGB",
|
||||
(
|
||||
(0, 15, 30),
|
||||
(120, 135, 150),
|
||||
(225, 240, 255),
|
||||
),
|
||||
),
|
||||
# P6 with maxval > 255
|
||||
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
||||
(
|
||||
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
||||
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
||||
"RGB",
|
||||
(
|
||||
(0, 1, 2),
|
||||
(127, 128, 129),
|
||||
(254, 255, 255),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_arbitrary_maxval(data, mode, pixels):
|
||||
fp = BytesIO(data)
|
||||
with Image.open(fp) as im:
|
||||
assert im.size == (3, 1)
|
||||
assert im.mode == mode
|
||||
|
||||
px = im.load()
|
||||
assert tuple(px[x, 0] for x in range(3)) == pixels
|
||||
|
||||
|
||||
def test_16bit_pgm():
|
||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||
im.load()
|
||||
assert im.mode == "I"
|
||||
assert im.size == (20, 100)
|
||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||
|
@ -32,8 +69,6 @@ def test_16bit_pgm():
|
|||
|
||||
def test_16bit_pgm_write(tmp_path):
|
||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||
im.load()
|
||||
|
||||
f = str(tmp_path / "temp.pgm")
|
||||
im.save(f, "PPM")
|
||||
|
||||
|
@ -82,17 +117,6 @@ def test_16bit_plain_pgm(tmp_path):
|
|||
assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
|
||||
|
||||
|
||||
def test_32bit_plain_pgm(tmp_path):
|
||||
# P2 with maxval 2 ** 31 - 1
|
||||
with Image.open("Tests/images/hopper_32bit_plain.pgm") as im:
|
||||
assert im.mode == "I"
|
||||
assert im.size == (128, 128)
|
||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||
|
||||
# P5 with maxval 2 ** 31 - 1
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper_32bit.pgm")
|
||||
|
||||
|
||||
def test_plain_pbm_data_with_comments(tmp_path):
|
||||
path1 = str(tmp_path / "temp1.ppm")
|
||||
path2 = str(tmp_path / "temp2.ppm")
|
||||
|
@ -222,22 +246,11 @@ def test_header_token_too_long(tmp_path):
|
|||
with Image.open(path):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "Token too long in file header: b'01234567890'"
|
||||
|
||||
|
||||
def test_too_many_colors(tmp_path):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P6\n1 1\n1000\n")
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
with Image.open(path):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "Too many colors for band: 1000"
|
||||
assert str(e.value) == "Token too long in file header: 01234567890"
|
||||
|
||||
|
||||
def test_truncated_header(tmp_path):
|
||||
# Test EOF in header
|
||||
path = str(tmp_path / "temp.pgm")
|
||||
with open(path, "w") as f:
|
||||
f.write("P6")
|
||||
|
@ -248,6 +261,25 @@ def test_truncated_header(tmp_path):
|
|||
|
||||
assert str(e.value) == "Reached EOF while reading header"
|
||||
|
||||
# Test EOF for PyDecoder
|
||||
fp = BytesIO(b"P5 3 1 4")
|
||||
with Image.open(fp) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("maxval", (0, 65536))
|
||||
def test_invalid_maxval(maxval, tmp_path):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "w") as f:
|
||||
f.write("P6\n3 1 " + str(maxval))
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
with Image.open(path):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
|
||||
|
||||
|
||||
def test_neg_ppm():
|
||||
# Storage.c accepted negative values for xsize, ysize. the
|
||||
|
|
|
@ -101,6 +101,10 @@ def test_cross_scan_line():
|
|||
with Image.open("Tests/images/cross_scan_line.tga") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
|
||||
|
||||
with Image.open("Tests/images/cross_scan_line_truncated.tga") as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_save(tmp_path):
|
||||
test_file = "Tests/images/tga_id_field.tga"
|
||||
|
|
|
@ -70,6 +70,15 @@ class TestFileTiff:
|
|||
im.load()
|
||||
im.close()
|
||||
|
||||
def test_seek_after_close(self):
|
||||
im = Image.open("Tests/images/multipage.tiff")
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.n_frames
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(1)
|
||||
|
||||
def test_context_manager(self):
|
||||
with warnings.catch_warnings():
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
|
@ -87,18 +96,38 @@ class TestFileTiff:
|
|||
|
||||
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
|
||||
|
||||
def test_bigtiff(self):
|
||||
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_name,mode,size,offset",
|
||||
"file_name,mode,size,tile",
|
||||
[
|
||||
("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160),
|
||||
("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8),
|
||||
(
|
||||
"tiff_wrong_bits_per_sample.tiff",
|
||||
"RGBA",
|
||||
(52, 53),
|
||||
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
|
||||
),
|
||||
(
|
||||
"tiff_wrong_bits_per_sample_2.tiff",
|
||||
"RGB",
|
||||
(16, 16),
|
||||
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
|
||||
),
|
||||
(
|
||||
"tiff_wrong_bits_per_sample_3.tiff",
|
||||
"RGBA",
|
||||
(512, 256),
|
||||
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_wrong_bits_per_sample(self, file_name, mode, size, offset):
|
||||
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
|
||||
with Image.open("Tests/images/" + file_name) as im:
|
||||
assert im.mode == mode
|
||||
assert im.size == size
|
||||
assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))]
|
||||
assert im.tile == tile
|
||||
im.load()
|
||||
|
||||
def test_set_legacy_api(self):
|
||||
|
@ -147,14 +176,14 @@ class TestFileTiff:
|
|||
assert im.info["dpi"] == (71.0, 71.0)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resolutionUnit, dpi",
|
||||
"resolution_unit, dpi",
|
||||
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
||||
)
|
||||
def test_load_float_dpi(self, resolutionUnit, dpi):
|
||||
def test_load_float_dpi(self, resolution_unit, dpi):
|
||||
with Image.open(
|
||||
"Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif"
|
||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||
) as im:
|
||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
|
||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
||||
assert im.info["dpi"] == (dpi, dpi)
|
||||
|
||||
def test_save_float_dpi(self, tmp_path):
|
||||
|
@ -221,6 +250,15 @@ class TestFileTiff:
|
|||
assert b[0] == ord(b"\x01")
|
||||
assert b[1] == ord(b"\xe0")
|
||||
|
||||
def test_16bit_r(self):
|
||||
with Image.open("Tests/images/16bit.r.tif") as im:
|
||||
assert im.getpixel((0, 0)) == 480
|
||||
assert im.mode == "I;16"
|
||||
|
||||
b = im.tobytes()
|
||||
assert b[0] == ord(b"\xe0")
|
||||
assert b[1] == ord(b"\x01")
|
||||
|
||||
def test_16bit_s(self):
|
||||
with Image.open("Tests/images/16bit.s.tif") as im:
|
||||
im.load()
|
||||
|
@ -459,6 +497,26 @@ class TestFileTiff:
|
|||
exif = im.getexif()
|
||||
check_exif(exif)
|
||||
|
||||
def test_modify_exif(self, tmp_path):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||
exif = im.getexif()
|
||||
exif[256] = 100
|
||||
|
||||
im.save(outfile, exif=exif)
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
exif = im.getexif()
|
||||
assert exif[256] == 100
|
||||
|
||||
def test_reload_exif_after_seek(self):
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
exif = im.getexif()
|
||||
del exif[256]
|
||||
im.seek(1)
|
||||
|
||||
assert 256 in exif
|
||||
|
||||
def test_exif_frames(self):
|
||||
# Test that EXIF data can change across frames
|
||||
with Image.open("Tests/images/g4-multi.tiff") as im:
|
||||
|
@ -598,6 +656,17 @@ class TestFileTiff:
|
|||
with Image.open(infile) as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||
|
||||
def test_planar_configuration_save(self, tmp_path):
|
||||
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
||||
with Image.open(infile) as im:
|
||||
assert im._planar_configuration == 2
|
||||
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, infile)
|
||||
|
||||
def test_palette(self, tmp_path):
|
||||
def roundtrip(mode):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
@ -631,11 +700,11 @@ class TestFileTiff:
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Test appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
mp = BytesIO()
|
||||
im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims))
|
||||
im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
|
||||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
|
@ -666,6 +735,13 @@ class TestFileTiff:
|
|||
with Image.open(outfile) as reloaded:
|
||||
assert reloaded.info["icc_profile"] == icc_profile
|
||||
|
||||
def test_save_bmp_compression(self, tmp_path):
|
||||
with Image.open("Tests/images/hopper.bmp") as im:
|
||||
assert im.info["compression"] == 0
|
||||
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile)
|
||||
|
||||
def test_discard_icc_profile(self, tmp_path):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
||||
|
|
|
@ -28,26 +28,26 @@ def test_rt_metadata(tmp_path):
|
|||
# For text items, we still have to decode('ascii','replace') because
|
||||
# the tiff file format can't take 8 bit bytes in that field.
|
||||
|
||||
basetextdata = "This is some arbitrary metadata for a text field"
|
||||
bindata = basetextdata.encode("ascii") + b" \xff"
|
||||
textdata = basetextdata + " " + chr(255)
|
||||
reloaded_textdata = basetextdata + " ?"
|
||||
floatdata = 12.345
|
||||
doubledata = 67.89
|
||||
base_text_data = "This is some arbitrary metadata for a text field"
|
||||
bin_data = base_text_data.encode("ascii") + b" \xff"
|
||||
text_data = base_text_data + " " + chr(255)
|
||||
reloaded_text_data = base_text_data + " ?"
|
||||
float_data = 12.345
|
||||
double_data = 67.89
|
||||
info = TiffImagePlugin.ImageFileDirectory()
|
||||
|
||||
ImageJMetaData = TAG_IDS["ImageJMetaData"]
|
||||
ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
|
||||
ImageDescription = TAG_IDS["ImageDescription"]
|
||||
|
||||
info[ImageJMetaDataByteCounts] = len(bindata)
|
||||
info[ImageJMetaData] = bindata
|
||||
info[TAG_IDS["RollAngle"]] = floatdata
|
||||
info[ImageJMetaDataByteCounts] = len(bin_data)
|
||||
info[ImageJMetaData] = bin_data
|
||||
info[TAG_IDS["RollAngle"]] = float_data
|
||||
info.tagtype[TAG_IDS["RollAngle"]] = 11
|
||||
info[TAG_IDS["YawAngle"]] = doubledata
|
||||
info[TAG_IDS["YawAngle"]] = double_data
|
||||
info.tagtype[TAG_IDS["YawAngle"]] = 12
|
||||
|
||||
info[ImageDescription] = textdata
|
||||
info[ImageDescription] = text_data
|
||||
|
||||
f = str(tmp_path / "temp.tif")
|
||||
|
||||
|
@ -55,28 +55,28 @@ def test_rt_metadata(tmp_path):
|
|||
|
||||
with Image.open(f) as loaded:
|
||||
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),)
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||
|
||||
assert loaded.tag[ImageJMetaData] == bindata
|
||||
assert loaded.tag_v2[ImageJMetaData] == bindata
|
||||
assert loaded.tag[ImageJMetaData] == bin_data
|
||||
assert loaded.tag_v2[ImageJMetaData] == bin_data
|
||||
|
||||
assert loaded.tag[ImageDescription] == (reloaded_textdata,)
|
||||
assert loaded.tag_v2[ImageDescription] == reloaded_textdata
|
||||
assert loaded.tag[ImageDescription] == (reloaded_text_data,)
|
||||
assert loaded.tag_v2[ImageDescription] == reloaded_text_data
|
||||
|
||||
loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
|
||||
assert round(abs(loaded_float - floatdata), 5) == 0
|
||||
assert round(abs(loaded_float - float_data), 5) == 0
|
||||
loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
|
||||
assert round(abs(loaded_double - doubledata), 7) == 0
|
||||
assert round(abs(loaded_double - double_data), 7) == 0
|
||||
|
||||
# check with 2 element ImageJMetaDataByteCounts, issue #2006
|
||||
|
||||
info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8)
|
||||
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
||||
img.save(f, tiffinfo=info)
|
||||
with Image.open(f) as loaded:
|
||||
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
|
||||
|
||||
def test_read_metadata():
|
||||
|
@ -356,7 +356,7 @@ def test_empty_values():
|
|||
assert 33432 in info
|
||||
|
||||
|
||||
def test_PhotoshopInfo(tmp_path):
|
||||
def test_photoshop_info(tmp_path):
|
||||
with Image.open("Tests/images/issue_2278.tif") as im:
|
||||
assert len(im.tag_v2[34377]) == 70
|
||||
assert isinstance(im.tag_v2[34377], bytes)
|
||||
|
|
|
@ -8,6 +8,7 @@ import pytest
|
|||
from PIL import Image, WebPImagePlugin, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
|
@ -105,6 +106,19 @@ class TestFileWebp:
|
|||
hopper().save(buffer_method, format="WEBP", method=6)
|
||||
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
||||
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_save_all(self, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = Image.new("RGB", (1, 1))
|
||||
im2 = Image.new("RGB", (1, 1), "#f00")
|
||||
im.save(temp_file, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
reloaded.seek(1)
|
||||
assert_image_similar(im2, reloaded, 1)
|
||||
|
||||
def test_icc_profile(self, tmp_path):
|
||||
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
||||
if _webp.HAVE_WEBPANIM:
|
||||
|
@ -171,9 +185,25 @@ class TestFileWebp:
|
|||
Image.open(blob).load()
|
||||
Image.open(blob).load()
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
@pytest.mark.parametrize(
|
||||
"background",
|
||||
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
|
||||
)
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_invalid_background(self, background, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = hopper()
|
||||
with pytest.raises(OSError):
|
||||
im.save(temp_file, save_all=True, append_images=[im], background=background)
|
||||
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_background_from_gif(self, tmp_path):
|
||||
# Save L mode GIF with background
|
||||
with Image.open("Tests/images/no_palette_with_background.gif") as im:
|
||||
out_webp = str(tmp_path / "temp.webp")
|
||||
im.save(out_webp, save_all=True)
|
||||
|
||||
# Save P mode GIF with background
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
original_value = im.convert("RGB").getpixel((1, 1))
|
||||
|
||||
|
@ -191,7 +221,6 @@ class TestFileWebp:
|
|||
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
|
||||
assert difference < 5
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_duration(self, tmp_path):
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -27,7 +28,6 @@ def test_n_frames():
|
|||
assert im.is_animated
|
||||
|
||||
|
||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
|
||||
def test_write_animation_L(tmp_path):
|
||||
"""
|
||||
Convert an animated GIF to animated WebP, then compare the frame count, and first
|
||||
|
@ -46,6 +46,11 @@ def test_write_animation_L(tmp_path):
|
|||
orig.load()
|
||||
im.load()
|
||||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||
|
||||
if is_big_endian():
|
||||
webp = parse_version(features.version_module("webp"))
|
||||
if webp < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
orig.seek(orig.n_frames - 1)
|
||||
im.seek(im.n_frames - 1)
|
||||
orig.load()
|
||||
|
@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path):
|
|||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||
|
||||
|
||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
|
||||
def test_write_animation_RGB(tmp_path):
|
||||
"""
|
||||
Write an animated WebP from RGB frames, and ensure the frames
|
||||
|
@ -69,6 +73,10 @@ def test_write_animation_RGB(tmp_path):
|
|||
assert_image_equal(im, frame1.convert("RGBA"))
|
||||
|
||||
# Compare second frame to original
|
||||
if is_big_endian():
|
||||
webp = parse_version(features.version_module("webp"))
|
||||
if webp < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
im.seek(1)
|
||||
im.load()
|
||||
assert_image_equal(im, frame2.convert("RGBA"))
|
||||
|
@ -82,14 +90,14 @@ def test_write_animation_RGB(tmp_path):
|
|||
check(temp_file1)
|
||||
|
||||
# Tests appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
temp_file2 = str(tmp_path / "temp_generator.webp")
|
||||
frame1.copy().save(
|
||||
temp_file2,
|
||||
save_all=True,
|
||||
append_images=imGenerator([frame2]),
|
||||
append_images=im_generator([frame2]),
|
||||
lossless=True,
|
||||
)
|
||||
check(temp_file2)
|
||||
|
|
|
@ -7,7 +7,7 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError
|
||||
from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -161,6 +161,8 @@ class TestImage:
|
|||
assert im.size == (128, 128)
|
||||
|
||||
for ext in (".jpg", ".jp2"):
|
||||
if ext == ".jp2" and not features.check_codec("jpg_2000"):
|
||||
pytest.skip("jpg_2000 not available")
|
||||
temp_file = str(tmp_path / ("temp." + ext))
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
|
@ -170,7 +172,7 @@ class TestImage:
|
|||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
||||
class FP:
|
||||
def write(a, b):
|
||||
def write(self, b):
|
||||
pass
|
||||
|
||||
fp = FP()
|
||||
|
@ -602,11 +604,34 @@ class TestImage:
|
|||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
assert_image_equal(im, im.remap_palette(list(range(256))))
|
||||
|
||||
# Test identity transform with an RGBA palette
|
||||
im = Image.new("P", (256, 1))
|
||||
for x in range(256):
|
||||
im.putpixel((x, 0), x)
|
||||
im.putpalette(list(range(256)) * 4, "RGBA")
|
||||
im_remapped = im.remap_palette(list(range(256)))
|
||||
assert_image_equal(im, im_remapped)
|
||||
assert im.palette.palette == im_remapped.palette.palette
|
||||
|
||||
# Test illegal image mode
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.remap_palette(None)
|
||||
|
||||
def test_remap_palette_transparency(self):
|
||||
im = Image.new("P", (1, 2))
|
||||
im.putpixel((0, 1), 1)
|
||||
im.info["transparency"] = 0
|
||||
|
||||
im_remapped = im.remap_palette([1, 0])
|
||||
assert im_remapped.info["transparency"] == 1
|
||||
|
||||
# Test unused transparency
|
||||
im.info["transparency"] = 2
|
||||
|
||||
im_remapped = im.remap_palette([1, 0])
|
||||
assert "transparency" not in im_remapped.info
|
||||
|
||||
def test__new(self):
|
||||
im = hopper("RGB")
|
||||
im_p = hopper("P")
|
||||
|
@ -652,6 +677,15 @@ class TestImage:
|
|||
with warnings.catch_warnings():
|
||||
im.save(temp_file)
|
||||
|
||||
def test_no_new_file_on_error(self, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
||||
im = Image.new("RGB", (0, 0))
|
||||
with pytest.raises(ValueError):
|
||||
im.save(temp_file)
|
||||
|
||||
assert not os.path.exists(temp_file)
|
||||
|
||||
def test_load_on_nonexclusive_multiframe(self):
|
||||
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
||||
|
||||
|
@ -666,6 +700,19 @@ class TestImage:
|
|||
|
||||
assert not fp.closed
|
||||
|
||||
def test_empty_exif(self):
|
||||
with Image.open("Tests/images/exif.png") as im:
|
||||
exif = im.getexif()
|
||||
assert dict(exif) != {}
|
||||
|
||||
# Test that exif data is cleared after another load
|
||||
exif.load(None)
|
||||
assert dict(exif) == {}
|
||||
|
||||
# Test loading just the EXIF header
|
||||
exif.load(b"Exif\x00\x00")
|
||||
assert dict(exif) == {}
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
@ -802,6 +849,35 @@ class TestImage:
|
|||
im = Image.new("RGB", size)
|
||||
assert im.tobytes() == b""
|
||||
|
||||
def test_apply_transparency(self):
|
||||
im = Image.new("P", (1, 1))
|
||||
im.putpalette((0, 0, 0, 1, 1, 1))
|
||||
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
|
||||
|
||||
# Test that no transformation is applied without transparency
|
||||
im.apply_transparency()
|
||||
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
|
||||
|
||||
# Test that a transparency index is applied
|
||||
im.info["transparency"] = 0
|
||||
im.apply_transparency()
|
||||
assert "transparency" not in im.info
|
||||
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1}
|
||||
|
||||
# Test that existing transparency is kept
|
||||
im = Image.new("P", (1, 1))
|
||||
im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
|
||||
im.info["transparency"] = 0
|
||||
im.apply_transparency()
|
||||
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1}
|
||||
|
||||
# Test that transparency bytes are applied
|
||||
with Image.open("Tests/images/pil123p.png") as im:
|
||||
assert isinstance(im.info["transparency"], bytes)
|
||||
assert im.palette.colors[(27, 35, 6)] == 24
|
||||
im.apply_transparency()
|
||||
assert im.palette.colors[(27, 35, 6, 214)] == 24
|
||||
|
||||
def test_categories_deprecation(self):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert hopper().category == 0
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ctypes
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest):
|
|||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0), None)
|
||||
with pytest.raises(IndexError):
|
||||
assert im.load() is not None
|
||||
|
||||
error = ValueError if self._need_cffi_access else IndexError
|
||||
with pytest.raises(error):
|
||||
im.putpixel((0, 0), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.putpixel((-1, -1), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
# check initial color
|
||||
|
@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest):
|
|||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0), c)
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
with pytest.raises(IndexError):
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
def test_basic(self):
|
||||
|
@ -401,6 +403,8 @@ class TestEmbeddable:
|
|||
"not from shell",
|
||||
)
|
||||
def test_embeddable(self):
|
||||
import ctypes
|
||||
|
||||
with open("embed_pil.c", "w") as fh:
|
||||
fh.write(
|
||||
"""
|
||||
|
|
|
@ -80,3 +80,15 @@ def test_fromarray():
|
|||
with pytest.raises(TypeError):
|
||||
wrapped = Wrapper(test("L"), {"shape": (100, 128)})
|
||||
Image.fromarray(wrapped)
|
||||
|
||||
|
||||
def test_fromarray_palette():
|
||||
# Arrange
|
||||
i = im.convert("L")
|
||||
a = numpy.array(i)
|
||||
|
||||
# Act
|
||||
out = Image.fromarray(a, "P")
|
||||
|
||||
# Assert that the Python and C palettes match
|
||||
assert len(out.palette.colors) == len(out.im.getpalette()) / 3
|
||||
|
|
|
@ -27,15 +27,15 @@ def test_sanity():
|
|||
"HSV",
|
||||
)
|
||||
|
||||
for mode in modes:
|
||||
im = hopper(mode)
|
||||
for mode in modes:
|
||||
convert(im, mode)
|
||||
for input_mode in modes:
|
||||
im = hopper(input_mode)
|
||||
for output_mode in modes:
|
||||
convert(im, output_mode)
|
||||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0))
|
||||
for mode in modes:
|
||||
convert(im, mode)
|
||||
im = Image.new(input_mode, (0, 0))
|
||||
for output_mode in modes:
|
||||
convert(im, output_mode)
|
||||
|
||||
|
||||
def test_default():
|
||||
|
@ -70,6 +70,11 @@ def test_16bit():
|
|||
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
||||
_test_float_conversion(im)
|
||||
|
||||
for color in (65535, 65536):
|
||||
im = Image.new("I", (1, 1), color)
|
||||
im_i16 = im.convert("I;16")
|
||||
assert im_i16.getpixel((0, 0)) == 65535
|
||||
|
||||
|
||||
def test_16bit_workaround():
|
||||
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
||||
|
@ -135,6 +140,10 @@ def test_trns_l(tmp_path):
|
|||
|
||||
f = str(tmp_path / "temp.png")
|
||||
|
||||
im_la = im.convert("LA")
|
||||
assert "transparency" not in im_la.info
|
||||
im_la.save(f)
|
||||
|
||||
im_rgb = im.convert("RGB")
|
||||
assert im_rgb.info["transparency"] == (128, 128, 128) # undone
|
||||
im_rgb.save(f)
|
||||
|
@ -213,6 +222,20 @@ def test_p_la():
|
|||
assert_image_similar(alpha, comparable, 5)
|
||||
|
||||
|
||||
def test_p2pa_alpha():
|
||||
with Image.open("Tests/images/tiny.png") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im_pa = im.convert("PA")
|
||||
assert im_pa.mode == "PA"
|
||||
|
||||
im_a = im_pa.getchannel("A")
|
||||
for x in range(4):
|
||||
alpha = 255 if x > 1 else 0
|
||||
for y in range(4):
|
||||
assert im_a.getpixel((x, y)) == alpha
|
||||
|
||||
|
||||
def test_matrix_illegal_conversion():
|
||||
# Arrange
|
||||
im = hopper("CMYK")
|
||||
|
|
|
@ -6,8 +6,8 @@ from .helper import hopper
|
|||
|
||||
|
||||
def test_copy():
|
||||
croppedCoordinates = (10, 10, 20, 20)
|
||||
croppedSize = (10, 10)
|
||||
cropped_coordinates = (10, 10, 20, 20)
|
||||
cropped_size = (10, 10)
|
||||
for mode in "1", "P", "L", "RGB", "I", "F":
|
||||
# Internal copy method
|
||||
im = hopper(mode)
|
||||
|
@ -23,15 +23,15 @@ def test_copy():
|
|||
|
||||
# Internal copy method on a cropped image
|
||||
im = hopper(mode)
|
||||
out = im.crop(croppedCoordinates).copy()
|
||||
out = im.crop(cropped_coordinates).copy()
|
||||
assert out.mode == im.mode
|
||||
assert out.size == croppedSize
|
||||
assert out.size == cropped_size
|
||||
|
||||
# Python's copy method on a cropped image
|
||||
im = hopper(mode)
|
||||
out = copy.copy(im.crop(croppedCoordinates))
|
||||
out = copy.copy(im.crop(cropped_coordinates))
|
||||
assert out.mode == im.mode
|
||||
assert out.size == croppedSize
|
||||
assert out.size == cropped_size
|
||||
|
||||
|
||||
def test_copy_zero():
|
||||
|
|
|
@ -99,10 +99,10 @@ def test_rankfilter_properties():
|
|||
|
||||
|
||||
def test_builtinfilter_p():
|
||||
builtinFilter = ImageFilter.BuiltinFilter()
|
||||
builtin_filter = ImageFilter.BuiltinFilter()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builtinFilter.filter(hopper("P"))
|
||||
builtin_filter.filter(hopper("P"))
|
||||
|
||||
|
||||
def test_kernel_not_enough_coefficients():
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageQt
|
||||
from PIL import Image
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_equal, cached_property
|
||||
from .helper import CachedProperty, assert_image_equal
|
||||
|
||||
|
||||
class TestImagingPaste:
|
||||
|
@ -34,7 +34,7 @@ class TestImagingPaste:
|
|||
im.paste(im2, mask)
|
||||
self.assert_9points_image(im, expected)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def mask_1(self):
|
||||
mask = Image.new("1", (self.size, self.size))
|
||||
px = mask.load()
|
||||
|
@ -43,11 +43,11 @@ class TestImagingPaste:
|
|||
px[y, x] = (x + y) % 2
|
||||
return mask
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def mask_L(self):
|
||||
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_L(self):
|
||||
gradient = Image.new("L", (self.size, self.size))
|
||||
px = gradient.load()
|
||||
|
@ -56,7 +56,7 @@ class TestImagingPaste:
|
|||
px[y, x] = (x + y) % 255
|
||||
return gradient
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_RGB(self):
|
||||
return Image.merge(
|
||||
"RGB",
|
||||
|
@ -67,7 +67,17 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_LA(self):
|
||||
return Image.merge(
|
||||
"LA",
|
||||
[
|
||||
self.gradient_L,
|
||||
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
|
||||
],
|
||||
)
|
||||
|
||||
@CachedProperty
|
||||
def gradient_RGBA(self):
|
||||
return Image.merge(
|
||||
"RGBA",
|
||||
|
@ -79,7 +89,7 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_RGBa(self):
|
||||
return Image.merge(
|
||||
"RGBa",
|
||||
|
@ -145,6 +155,28 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
def test_image_mask_LA(self):
|
||||
for mode in ("RGBA", "RGB", "L"):
|
||||
im = Image.new(mode, (200, 200), "white")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
self.assert_9points_paste(
|
||||
im,
|
||||
im2,
|
||||
self.gradient_LA,
|
||||
[
|
||||
(128, 191, 255, 191),
|
||||
(112, 207, 206, 111),
|
||||
(128, 254, 128, 1),
|
||||
(208, 208, 239, 239),
|
||||
(192, 191, 191, 191),
|
||||
(207, 207, 112, 113),
|
||||
(255, 255, 255, 255),
|
||||
(239, 207, 207, 239),
|
||||
(255, 191, 128, 191),
|
||||
],
|
||||
)
|
||||
|
||||
def test_image_mask_RGBA(self):
|
||||
for mode in ("RGBA", "RGB", "L"):
|
||||
im = Image.new(mode, (200, 200), "white")
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
|
||||
|
||||
|
@ -10,17 +12,31 @@ def test_sanity():
|
|||
im.point(list(range(256)))
|
||||
im.point(list(range(256)) * 3)
|
||||
im.point(lambda x: x)
|
||||
im.point(lambda x: x * 1.2)
|
||||
|
||||
im = im.convert("I")
|
||||
with pytest.raises(ValueError):
|
||||
im.point(list(range(256)))
|
||||
im.point(lambda x: x * 1)
|
||||
im.point(lambda x: x + 1)
|
||||
im.point(lambda x: x - 1)
|
||||
im.point(lambda x: x * 1 + 1)
|
||||
im.point(lambda x: 0.1 + 0.2 * x)
|
||||
im.point(lambda x: -x)
|
||||
im.point(lambda x: x - 0.5)
|
||||
im.point(lambda x: 1 - x / 2)
|
||||
im.point(lambda x: (2 + x) / 3)
|
||||
im.point(lambda x: 0.5)
|
||||
im.point(lambda x: x / 1)
|
||||
im.point(lambda x: x + x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: x - 1)
|
||||
im.point(lambda x: x * x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: x / 1)
|
||||
im.point(lambda x: x / x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: 1 / x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: x // 2)
|
||||
|
||||
|
||||
def test_16bit_lut():
|
||||
|
@ -46,3 +62,8 @@ def test_f_mode():
|
|||
im = hopper("F")
|
||||
with pytest.raises(ValueError):
|
||||
im.point(None)
|
||||
|
||||
|
||||
def test_coerce_e_deprecation():
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert Image.coerce_e(2).data == 2
|
||||
|
|
|
@ -458,7 +458,7 @@ class TestCoreResampleBox:
|
|||
def split_range(size, tiles):
|
||||
scale = size / tiles
|
||||
for i in range(tiles):
|
||||
yield (int(round(scale * i)), int(round(scale * (i + 1))))
|
||||
yield int(round(scale * i)), int(round(scale * (i + 1)))
|
||||
|
||||
tiled = Image.new(im.mode, dst_size)
|
||||
scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1])
|
||||
|
|
|
@ -12,6 +12,7 @@ from .helper import (
|
|||
assert_image_equal_tofile,
|
||||
assert_image_similar,
|
||||
hopper,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
|
||||
|
@ -264,6 +265,14 @@ class TestImageResize:
|
|||
with pytest.raises(ValueError):
|
||||
im.resize((10, 10), "unknown")
|
||||
|
||||
@skip_unless_feature("libtiff")
|
||||
def test_load_first(self):
|
||||
# load() may change the size of the image
|
||||
# Test that resize() is calling it before getting the size
|
||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||
im = im.resize((64, 64))
|
||||
assert im.size == (64, 64)
|
||||
|
||||
def test_default_filter(self):
|
||||
for mode in "L", "RGB", "I", "F":
|
||||
im = hopper(mode)
|
||||
|
|
|
@ -7,6 +7,7 @@ from .helper import (
|
|||
assert_image_similar,
|
||||
fromstring,
|
||||
hopper,
|
||||
skip_unless_feature,
|
||||
tostring,
|
||||
)
|
||||
|
||||
|
@ -88,6 +89,15 @@ def test_no_resize():
|
|||
assert im.size == (64, 64)
|
||||
|
||||
|
||||
@skip_unless_feature("libtiff")
|
||||
def test_load_first():
|
||||
# load() may change the size of the image
|
||||
# Test that thumbnail() is calling it before performing size calculations
|
||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||
im.thumbnail((64, 64))
|
||||
assert im.size == (64, 10)
|
||||
|
||||
|
||||
# valgrind test is failing with memory allocated in libjpeg
|
||||
@pytest.mark.valgrind_known_error(reason="Known Failing")
|
||||
def test_DCT_scaling_edges():
|
||||
|
@ -130,4 +140,4 @@ def test_reducing_gap_for_DCT_scaling():
|
|||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
|
||||
|
||||
assert_image_equal(ref, im)
|
||||
assert_image_similar(ref, im, 1.4)
|
||||
|
|
|
@ -655,6 +655,20 @@ def test_polygon_1px_high():
|
|||
assert_image_equal_tofile(im, expected)
|
||||
|
||||
|
||||
def test_polygon_1px_high_translucent():
|
||||
# Test drawing a translucent 1px high polygon
|
||||
# Arrange
|
||||
im = Image.new("RGB", (4, 3))
|
||||
draw = ImageDraw.Draw(im, "RGBA")
|
||||
expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png"
|
||||
|
||||
# Act
|
||||
draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127))
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im, expected)
|
||||
|
||||
|
||||
def test_polygon_translucent():
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
|
@ -1440,3 +1454,23 @@ def test_continuous_horizontal_edges_polygon():
|
|||
assert_image_equal_tofile(
|
||||
img, expected, "continuous horizontal edges polygon failed"
|
||||
)
|
||||
|
||||
|
||||
def test_discontiguous_corners_polygon():
|
||||
img, draw = create_base_image_draw((84, 68))
|
||||
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
|
||||
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
|
||||
draw.polygon(
|
||||
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
|
||||
BLACK,
|
||||
)
|
||||
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
||||
assert_image_similar_tofile(img, expected, 1)
|
||||
|
||||
|
||||
def test_polygon():
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
|
||||
expected = "Tests/images/imagedraw_outline_polygon_RGB.png"
|
||||
assert_image_similar_tofile(im, expected, 1)
|
||||
|
|
|
@ -2,7 +2,15 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features
|
||||
from PIL import (
|
||||
BmpImagePlugin,
|
||||
EpsImagePlugin,
|
||||
Image,
|
||||
ImageFile,
|
||||
UnidentifiedImageError,
|
||||
_binary,
|
||||
features,
|
||||
)
|
||||
|
||||
from .helper import (
|
||||
assert_image,
|
||||
|
@ -35,9 +43,9 @@ class TestImageFile:
|
|||
|
||||
parser = ImageFile.Parser()
|
||||
parser.feed(data)
|
||||
imOut = parser.close()
|
||||
im_out = parser.close()
|
||||
|
||||
return im, imOut
|
||||
return im, im_out
|
||||
|
||||
assert_image_equal(*roundtrip("BMP"))
|
||||
im1, im2 = roundtrip("GIF")
|
||||
|
@ -200,6 +208,9 @@ class MockPyEncoder(ImageFile.PyEncoder):
|
|||
def encode(self, buffer):
|
||||
return 1, 1, b""
|
||||
|
||||
def cleanup(self):
|
||||
self.cleanup_called = True
|
||||
|
||||
|
||||
xoff, yoff, xsize, ysize = 10, 20, 100, 100
|
||||
|
||||
|
@ -327,10 +338,12 @@ class TestPyEncoder(CodecsTest):
|
|||
im = MockImageFile(buf)
|
||||
|
||||
fp = BytesIO()
|
||||
self.encoder.cleanup_called = False
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
|
||||
)
|
||||
assert self.encoder.cleanup_called
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImageFile._save(
|
||||
|
@ -372,3 +385,7 @@ class TestPyEncoder(CodecsTest):
|
|||
|
||||
with pytest.raises(NotImplementedError):
|
||||
encoder.encode_to_file(None, None)
|
||||
|
||||
def test_zero_height(self):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
Image.open("Tests/images/zero_height.j2k")
|
||||
|
|
|
@ -65,9 +65,12 @@ class TestImageFont:
|
|||
return font_bytes
|
||||
|
||||
def test_font_with_filelike(self):
|
||||
ImageFont.truetype(
|
||||
ttf = ImageFont.truetype(
|
||||
self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
|
||||
)
|
||||
ttf_copy = ttf.font_variant()
|
||||
assert ttf_copy.font_bytes == ttf.font_bytes
|
||||
|
||||
self._render(self._font_as_bytes())
|
||||
# Usage note: making two fonts from the same buffer fails.
|
||||
# shared_bytes = self._font_as_bytes()
|
||||
|
@ -977,6 +980,14 @@ class TestImageFont:
|
|||
|
||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
|
||||
|
||||
def test_fill_deprecation(self):
|
||||
font = self.get_font()
|
||||
with pytest.warns(DeprecationWarning):
|
||||
font.getmask2("Hello world", fill=Image.core.fill)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.raises(TypeError):
|
||||
font.getmask2("Hello world", fill=None)
|
||||
|
||||
|
||||
@skip_unless_feature("raqm")
|
||||
class TestImageFont_RaqmLayout(TestImageFont):
|
||||
|
|
|
@ -48,12 +48,8 @@ def img_string_normalize(im):
|
|||
return img_to_string(string_to_img(im))
|
||||
|
||||
|
||||
def assert_img_equal(A, B):
|
||||
assert img_to_string(A) == img_to_string(B)
|
||||
|
||||
|
||||
def assert_img_equal_img_string(A, Bstring):
|
||||
assert img_to_string(A) == img_string_normalize(Bstring)
|
||||
def assert_img_equal_img_string(a, b_string):
|
||||
assert img_to_string(a) == img_string_normalize(b_string)
|
||||
|
||||
|
||||
def test_str_to_img():
|
||||
|
|
|
@ -63,6 +63,7 @@ def test_sanity():
|
|||
ImageOps.grayscale(hopper("L"))
|
||||
ImageOps.grayscale(hopper("RGB"))
|
||||
|
||||
ImageOps.invert(hopper("1"))
|
||||
ImageOps.invert(hopper("L"))
|
||||
ImageOps.invert(hopper("RGB"))
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ def test_overflow_segfault():
|
|||
# through to the sequence. Seeing this on 32-bit Windows.
|
||||
with pytest.raises((TypeError, MemoryError)):
|
||||
# post patch, this fails with a memory error
|
||||
x = evil()
|
||||
x = Evil()
|
||||
|
||||
# This fails due to the invalid malloc above,
|
||||
# and segfaults
|
||||
|
@ -182,7 +182,7 @@ def test_overflow_segfault():
|
|||
x[i] = b"0" * 16
|
||||
|
||||
|
||||
class evil:
|
||||
class Evil:
|
||||
def __init__(self):
|
||||
self.corrupt = Image.core.path(0x4000000000000000)
|
||||
|
||||
|
|
|
@ -2,10 +2,13 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_similar, hopper
|
||||
|
||||
with warnings.catch_warnings() as w:
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
||||
)
|
||||
|
|
|
@ -65,21 +65,21 @@ def test_libtiff():
|
|||
|
||||
def test_consecutive():
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
firstFrame = None
|
||||
first_frame = None
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
if firstFrame is None:
|
||||
firstFrame = frame.copy()
|
||||
if first_frame is None:
|
||||
first_frame = frame.copy()
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
assert_image_equal(frame, firstFrame)
|
||||
assert_image_equal(frame, first_frame)
|
||||
break
|
||||
|
||||
|
||||
def test_palette_mmap():
|
||||
# Using mmap in ImageFile can require to reload the palette.
|
||||
with Image.open("Tests/images/multipage-mmap.tiff") as im:
|
||||
color1 = im.getpalette()[0:3]
|
||||
color1 = im.getpalette()[:3]
|
||||
im.seek(0)
|
||||
color2 = im.getpalette()[0:3]
|
||||
color2 = im.getpalette()[:3]
|
||||
assert color1 == color2
|
||||
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ def setup_module():
|
|||
# setup tk
|
||||
tk.Frame()
|
||||
# root = tk.Tk()
|
||||
except RuntimeError as v:
|
||||
pytest.skip(f"RuntimeError: {v}")
|
||||
except tk.TclError as v:
|
||||
pytest.skip(f"TCL Error: {v}")
|
||||
|
||||
|
@ -75,8 +77,16 @@ def test_photoimage_blank():
|
|||
assert im_tk.width() == 100
|
||||
assert im_tk.height() == 100
|
||||
|
||||
# reloaded = ImageTk.getimage(im_tk)
|
||||
# assert_image_equal(reloaded, im)
|
||||
im = Image.new(mode, (100, 100))
|
||||
reloaded = ImageTk.getimage(im_tk)
|
||||
assert_image_equal(reloaded.convert(mode), im)
|
||||
|
||||
|
||||
def test_box_deprecation():
|
||||
im = hopper()
|
||||
im_tk = ImageTk.PhotoImage(im)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
im_tk.paste(im, (0, 0, 128, 128))
|
||||
|
||||
|
||||
def test_bitmapimage():
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import ctypes
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image, ImageWin
|
||||
|
@ -8,6 +7,7 @@ from .helper import hopper, is_win32
|
|||
# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
|
||||
|
||||
if is_win32():
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
|
||||
class BITMAPFILEHEADER(ctypes.Structure):
|
||||
|
|
|
@ -444,6 +444,8 @@ class TestLibUnpack:
|
|||
self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
|
||||
self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
|
||||
self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
|
||||
self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16))
|
||||
self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15))
|
||||
self.assert_unpack(
|
||||
"RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
|
||||
)
|
||||
|
|
|
@ -26,51 +26,51 @@ def test_basic(tmp_path):
|
|||
|
||||
def basic(mode):
|
||||
|
||||
imIn = original.convert(mode)
|
||||
verify(imIn)
|
||||
im_in = original.convert(mode)
|
||||
verify(im_in)
|
||||
|
||||
w, h = imIn.size
|
||||
w, h = im_in.size
|
||||
|
||||
imOut = imIn.copy()
|
||||
verify(imOut) # copy
|
||||
im_out = im_in.copy()
|
||||
verify(im_out) # copy
|
||||
|
||||
imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
|
||||
verify(imOut) # transform
|
||||
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
|
||||
verify(im_out) # transform
|
||||
|
||||
filename = str(tmp_path / "temp.im")
|
||||
imIn.save(filename)
|
||||
im_in.save(filename)
|
||||
|
||||
with Image.open(filename) as imOut:
|
||||
with Image.open(filename) as im_out:
|
||||
|
||||
verify(imIn)
|
||||
verify(imOut)
|
||||
verify(im_in)
|
||||
verify(im_out)
|
||||
|
||||
imOut = imIn.crop((0, 0, w, h))
|
||||
verify(imOut)
|
||||
im_out = im_in.crop((0, 0, w, h))
|
||||
verify(im_out)
|
||||
|
||||
imOut = Image.new(mode, (w, h), None)
|
||||
imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0))
|
||||
imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0))
|
||||
im_out = Image.new(mode, (w, h), None)
|
||||
im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
|
||||
im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
|
||||
|
||||
verify(imIn)
|
||||
verify(imOut)
|
||||
verify(im_in)
|
||||
verify(im_out)
|
||||
|
||||
imIn = Image.new(mode, (1, 1), 1)
|
||||
assert imIn.getpixel((0, 0)) == 1
|
||||
im_in = Image.new(mode, (1, 1), 1)
|
||||
assert im_in.getpixel((0, 0)) == 1
|
||||
|
||||
imIn.putpixel((0, 0), 2)
|
||||
assert imIn.getpixel((0, 0)) == 2
|
||||
im_in.putpixel((0, 0), 2)
|
||||
assert im_in.getpixel((0, 0)) == 2
|
||||
|
||||
if mode == "L":
|
||||
maximum = 255
|
||||
else:
|
||||
maximum = 32767
|
||||
|
||||
imIn = Image.new(mode, (1, 1), 256)
|
||||
assert imIn.getpixel((0, 0)) == min(256, maximum)
|
||||
im_in = Image.new(mode, (1, 1), 256)
|
||||
assert im_in.getpixel((0, 0)) == min(256, maximum)
|
||||
|
||||
imIn.putpixel((0, 0), 512)
|
||||
assert imIn.getpixel((0, 0)) == min(512, maximum)
|
||||
im_in.putpixel((0, 0), 512)
|
||||
assert im_in.getpixel((0, 0)) == min(512, maximum)
|
||||
|
||||
basic("L")
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import ImageQt
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import ImageQt
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ def test_is_path():
|
|||
fp = "filename.ext"
|
||||
|
||||
# Act
|
||||
it_is = _util.isPath(fp)
|
||||
it_is = _util.is_path(fp)
|
||||
|
||||
# Assert
|
||||
assert it_is
|
||||
|
@ -21,7 +21,7 @@ def test_path_obj_is_path():
|
|||
test_path = Path("filename.ext")
|
||||
|
||||
# Act
|
||||
it_is = _util.isPath(test_path)
|
||||
it_is = _util.is_path(test_path)
|
||||
|
||||
# Assert
|
||||
assert it_is
|
||||
|
@ -33,7 +33,7 @@ def test_is_not_path(tmp_path):
|
|||
pass
|
||||
|
||||
# Act
|
||||
it_is_not = _util.isPath(fp)
|
||||
it_is_not = _util.is_path(fp)
|
||||
|
||||
# Assert
|
||||
assert not it_is_not
|
||||
|
@ -44,7 +44,7 @@ def test_is_directory():
|
|||
directory = "Tests"
|
||||
|
||||
# Act
|
||||
it_is = _util.isDirectory(directory)
|
||||
it_is = _util.is_directory(directory)
|
||||
|
||||
# Assert
|
||||
assert it_is
|
||||
|
@ -55,7 +55,7 @@ def test_is_not_directory():
|
|||
text = "abc"
|
||||
|
||||
# Act
|
||||
it_is_not = _util.isDirectory(text)
|
||||
it_is_not = _util.is_directory(text)
|
||||
|
||||
# Assert
|
||||
assert not it_is_not
|
||||
|
@ -65,7 +65,7 @@ def test_deferred_error():
|
|||
# Arrange
|
||||
|
||||
# Act
|
||||
thing = _util.deferred_error(ValueError("Some error text"))
|
||||
thing = _util.DeferredError(ValueError("Some error text"))
|
||||
|
||||
# Assert
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install openjpeg
|
||||
|
||||
archive=openjpeg-2.4.0
|
||||
archive=openjpeg-2.5.0
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
17
docs/conf.py
|
@ -16,8 +16,6 @@
|
|||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
import sphinx_rtd_theme
|
||||
|
||||
import PIL
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
@ -70,7 +68,7 @@ release = PIL.__version__
|
|||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = "en"
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
|
@ -126,13 +124,15 @@ nitpicky = True
|
|||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
html_theme = "furo"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
html_theme_options = {
|
||||
"light_logo": "pillow-logo-dark-text.png",
|
||||
"dark_logo": "pillow-logo.png",
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
@ -146,7 +146,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
html_logo = "resources/pillow-logo.png"
|
||||
# html_logo = "resources/pillow-logo.png"
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
|
@ -311,10 +311,7 @@ texinfo_documents = [
|
|||
|
||||
|
||||
def setup(app):
|
||||
app.add_js_file("js/script.js")
|
||||
app.add_css_file("css/styles.css")
|
||||
app.add_css_file("css/dark.css")
|
||||
app.add_css_file("css/light.css")
|
||||
|
||||
|
||||
# GitHub repo for sphinx-issues
|
||||
|
|
|
@ -69,7 +69,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
|
|||
Constants
|
||||
~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
.. deprecated:: 9.1.0
|
||||
|
||||
A number of constants have been deprecated and will be removed in Pillow 10.0.0
|
||||
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
|
||||
|
@ -97,8 +97,8 @@ Deprecated Use instead
|
|||
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
|
||||
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
|
||||
``Image.BOX`` ``Image.Resampling.BOX``
|
||||
``Image.BILINEAR`` ``Image.Resampling.BILNEAR``
|
||||
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
|
||||
``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
|
||||
``Image.LINEAR`` ``Image.Resampling.BILINEAR``
|
||||
``Image.HAMMING`` ``Image.Resampling.HAMMING``
|
||||
``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
|
||||
``Image.CUBIC`` ``Image.Resampling.BICUBIC``
|
||||
|
@ -142,6 +142,42 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
|
|||
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
|
||||
:mod:`~PIL.FitsImagePlugin` instead.
|
||||
|
||||
FreeTypeFont.getmask2 fill parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
|
||||
deprecated and will be removed in Pillow 10 (2023-07-01).
|
||||
|
||||
PhotoImage.paste box parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
|
||||
|
||||
PyQt5 and PySide2
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
|
||||
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
|
||||
|
||||
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
|
||||
in Pillow 10 (2023-07-01). Upgrade to
|
||||
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
|
||||
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
|
||||
|
||||
Image.coerce_e
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
This undocumented method has been deprecated and will be removed in Pillow 10
|
||||
(2023-07-01).
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -253,7 +253,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
|
|||
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated DDS file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
class DXT5Decoder(ImageFile.PyDecoder):
|
||||
|
@ -264,7 +264,7 @@ class DXT5Decoder(ImageFile.PyDecoder):
|
|||
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
|
||||
except struct.error as e:
|
||||
raise OSError("Truncated DDS file") from e
|
||||
return 0, 0
|
||||
return -1, 0
|
||||
|
||||
|
||||
Image.register_decoder("DXT1", DXT1Decoder)
|
||||
|
|
|
@ -17,15 +17,13 @@ When an image is opened from a file, only that instance of the image is consider
|
|||
have the format. Copies of the image will contain data loaded from the file, but not
|
||||
the file itself, meaning that it can no longer be considered to be in the original
|
||||
format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
|
||||
internally creates a copy of the image, the ``fp`` (file pointer), along with any
|
||||
methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format`
|
||||
attribute will be ``None``.
|
||||
internally creates a copy of the image, then any methods or attributes specific to the
|
||||
format will no longer be present. The ``fp`` (file pointer) attribute will no longer be
|
||||
present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
|
||||
|
||||
Fully supported formats
|
||||
-----------------------
|
||||
|
||||
.. contents::
|
||||
|
||||
BLP
|
||||
^^^
|
||||
|
||||
|
@ -44,8 +42,9 @@ BMP
|
|||
^^^
|
||||
|
||||
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
|
||||
or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding
|
||||
is not supported.
|
||||
or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
|
||||
is not supported. Support for reading 8-bit run-length encoding was added in Pillow
|
||||
9.1.0.
|
||||
|
||||
The :py:meth:`~PIL.Image.open` method sets the following
|
||||
:py:attr:`~PIL.Image.Image.info` properties:
|
||||
|
@ -102,12 +101,38 @@ GIF
|
|||
^^^
|
||||
|
||||
Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
|
||||
writes run-length encoded files in GIF87a by default, unless GIF89a features
|
||||
are used or GIF89a is already in use.
|
||||
writes files in GIF87a by default, unless GIF89a features are used or GIF89a is
|
||||
already in use. Files are written with LZW encoding.
|
||||
|
||||
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
|
||||
images, but seeking to later frames in an image will change the mode to either
|
||||
``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
|
||||
images. Seeking to later frames in a ``P`` image will change the image to
|
||||
``RGB`` (or ``RGBA`` if the first frame had transparency).
|
||||
|
||||
``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain
|
||||
its own individual palette of up to 256 colors. When a new frame is placed onto a
|
||||
previous frame, those colors may combine to exceed the ``P`` mode limit of 256
|
||||
colors. Instead, the image is converted to ``RGB`` handle this.
|
||||
|
||||
If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that
|
||||
every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting
|
||||
available::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
|
||||
GIF frames do not always contain individual palettes however. If there is only
|
||||
a global palette, then all of the colors can fit within ``P`` mode. If you would
|
||||
prefer the frames to be kept as ``P`` in that case, there is also a setting
|
||||
available::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
|
||||
To restore the default behavior, where ``P`` mode images are only converted to
|
||||
``RGB`` or ``RGBA`` after the first frame::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
The :py:meth:`~PIL.Image.open` method sets the following
|
||||
:py:attr:`~PIL.Image.Image.info` properties:
|
||||
|
@ -131,7 +156,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
|
|||
it will loop forever.
|
||||
|
||||
**comment**
|
||||
May not be present. A comment about the image.
|
||||
May not be present. A comment about the image. This is the last comment found
|
||||
before the current frame's image.
|
||||
|
||||
**extension**
|
||||
May not be present. Contains application specific information.
|
||||
|
@ -220,17 +246,14 @@ Reading local images
|
|||
|
||||
The GIF loader creates an image memory the same size as the GIF file’s *logical
|
||||
screen size*, and pastes the actual pixel data (the *local image*) into this
|
||||
image. If you only want the actual pixel rectangle, you can manipulate the
|
||||
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.ImageFile.ImageFile.tile`
|
||||
attributes before loading the file::
|
||||
image. If you only want the actual pixel rectangle, you can crop the image::
|
||||
|
||||
im = Image.open(...)
|
||||
|
||||
if im.tile[0][0] == "gif":
|
||||
# only read the first "local image" from this GIF file
|
||||
tag, (x0, y0, x1, y1), offset, extra = im.tile[0]
|
||||
im.size = (x1 - x0, y1 - y0)
|
||||
im.tile = [(tag, (0, 0) + im.size, offset, extra)]
|
||||
box = im.tile[0][1]
|
||||
im = im.crop(box)
|
||||
|
||||
ICNS
|
||||
^^^^
|
||||
|
@ -364,10 +387,12 @@ The :py:meth:`~PIL.Image.open` method may set the following
|
|||
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||
|
||||
**quality**
|
||||
The image quality, on a scale from 0 (worst) to 95 (best). The default is
|
||||
75. Values above 95 should be avoided; 100 disables portions of the JPEG
|
||||
compression algorithm, and results in large files with hardly any gain in
|
||||
image quality.
|
||||
The image quality, on a scale from 0 (worst) to 95 (best), or the string
|
||||
``keep``. The default is 75. Values above 95 should be avoided; 100 disables
|
||||
portions of the JPEG compression algorithm, and results in large files with
|
||||
hardly any gain in image quality. The value ``keep`` is only valid for JPEG
|
||||
files and will retain the original image quality level, subsampling, and
|
||||
qtables.
|
||||
|
||||
**optimize**
|
||||
If present and true, indicates that the encoder should make an extra pass
|
||||
|
@ -475,9 +500,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
and must be greater than the code-block size.
|
||||
|
||||
**irreversible**
|
||||
If ``True``, use the lossy Irreversible Color Transformation
|
||||
followed by DWT 9-7. Defaults to ``False``, which means to use the
|
||||
Reversible Color Transformation with DWT 5-3.
|
||||
If ``True``, use the lossy discrete waveform transformation DWT 9-7.
|
||||
Defaults to ``False``, which uses the lossless DWT 5-3.
|
||||
|
||||
**mct**
|
||||
If ``1`` then enable multiple component transformation when encoding,
|
||||
otherwise use ``0`` for no component transformation (default). If MCT is
|
||||
enabled and ``irreversible`` is ``True`` then the Irreversible Color
|
||||
Transformation will be applied, otherwise encoding will use the
|
||||
Reversible Color Transformation. MCT works best with a ``mode`` of
|
||||
``RGB`` and is only applicable when the image data has 3 components.
|
||||
|
||||
.. versionadded:: 9.1.0
|
||||
|
||||
**progression**
|
||||
Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
|
||||
|
@ -497,6 +531,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
for compliant 4K files, *at least one* of the dimensions must match
|
||||
4096 x 2160.
|
||||
|
||||
**no_jp2**
|
||||
If ``True`` then don't wrap the raw codestream in the JP2 file format when
|
||||
saving, otherwise the extension of the filename will be used to determine
|
||||
the format (default).
|
||||
|
||||
.. versionadded:: 9.1.0
|
||||
|
||||
.. note::
|
||||
|
||||
To enable JPEG 2000 support, you need to build and install the OpenJPEG
|
||||
|
@ -743,7 +784,7 @@ parameter must be set to ``True``. The following parameters can also be set:
|
|||
PPM
|
||||
^^^
|
||||
|
||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or
|
||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
|
||||
``RGB`` data.
|
||||
|
||||
SGI
|
||||
|
@ -1192,6 +1233,11 @@ PSD
|
|||
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
|
||||
|
||||
|
||||
SUN
|
||||
^^^
|
||||
|
||||
Pillow identifies and reads Sun raster files.
|
||||
|
||||
WAL
|
||||
^^^
|
||||
|
||||
|
@ -1206,13 +1252,13 @@ this format.
|
|||
By default, a Quake2 standard palette is attached to the texture. To override
|
||||
the palette, use the putpalette method.
|
||||
|
||||
WMF
|
||||
^^^
|
||||
WMF, EMF
|
||||
^^^^^^^^
|
||||
|
||||
Pillow can identify WMF files.
|
||||
Pillow can identify WMF and EMF files.
|
||||
|
||||
On Windows, it can read WMF files. By default, it will load the image at 72
|
||||
dpi. To load it at another resolution:
|
||||
On Windows, it can read WMF and EMF files. By default, it will load the image
|
||||
at 72 dpi. To load it at another resolution:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -1222,7 +1268,8 @@ dpi. To load it at another resolution:
|
|||
im.load(dpi=144)
|
||||
|
||||
To add other read or write support, use
|
||||
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.
|
||||
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
|
||||
handler.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
|
@ -171,20 +171,37 @@ Rolling an image
|
|||
|
||||
::
|
||||
|
||||
def roll(image, delta):
|
||||
def roll(im, delta):
|
||||
"""Roll an image sideways."""
|
||||
xsize, ysize = image.size
|
||||
xsize, ysize = im.size
|
||||
|
||||
delta = delta % xsize
|
||||
if delta == 0:
|
||||
return image
|
||||
return im
|
||||
|
||||
part1 = image.crop((0, 0, delta, ysize))
|
||||
part2 = image.crop((delta, 0, xsize, ysize))
|
||||
image.paste(part1, (xsize - delta, 0, xsize, ysize))
|
||||
image.paste(part2, (0, 0, xsize - delta, ysize))
|
||||
part1 = im.crop((0, 0, delta, ysize))
|
||||
part2 = im.crop((delta, 0, xsize, ysize))
|
||||
im.paste(part1, (xsize - delta, 0, xsize, ysize))
|
||||
im.paste(part2, (0, 0, xsize - delta, ysize))
|
||||
|
||||
return image
|
||||
return im
|
||||
|
||||
Or if you would like to merge two images into a wider image:
|
||||
|
||||
Merging images
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
def merge(im1, im2):
|
||||
w = im1.size[0] + im2.size[0]
|
||||
h = max(im1.size[1], im2.size[1])
|
||||
im = Image.new("RGBA", (w, h))
|
||||
|
||||
im.paste(im1)
|
||||
im.paste(im2, (im1.size[0], 0))
|
||||
|
||||
return im
|
||||
|
||||
For more advanced tricks, the paste method can also take a transparency mask as
|
||||
an optional argument. In this mask, the value 255 indicates that the pasted
|
||||
|
@ -487,6 +504,17 @@ image header. In addition, seek will also be used when the image data is read
|
|||
tar file, you can use the :py:class:`~PIL.ContainerIO` or
|
||||
:py:class:`~PIL.TarIO` modules to access it.
|
||||
|
||||
Reading from URL
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
from PIL import Image
|
||||
from urllib.request import urlopen
|
||||
url = "https://python-pillow.org/images/pillow-logo.png"
|
||||
img = Image.open(urlopen(url))
|
||||
|
||||
|
||||
Reading from a tar archive
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -123,8 +123,12 @@ The ``tile`` attribute
|
|||
To be able to read the file as well as just identifying it, the ``tile``
|
||||
attribute must also be set. This attribute consists of a list of tile
|
||||
descriptors, where each descriptor specifies how data should be loaded to a
|
||||
given region in the image. In most cases, only a single descriptor is used,
|
||||
covering the full image.
|
||||
given region in the image.
|
||||
|
||||
In most cases, only a single descriptor is used, covering the full image.
|
||||
:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine
|
||||
channels within a single layer, given that the channels are stored separately,
|
||||
one after the other.
|
||||
|
||||
The tile descriptor is a 4-tuple with the following contents::
|
||||
|
||||
|
@ -324,42 +328,42 @@ The fields are used as follows:
|
|||
Whether the first line in the image is the top line on the screen (1), or
|
||||
the bottom line (-1). If omitted, the orientation defaults to 1.
|
||||
|
||||
.. _file-decoders:
|
||||
.. _file-codecs:
|
||||
|
||||
Writing Your Own File Decoder in C
|
||||
==================================
|
||||
Writing Your Own File Codec in C
|
||||
================================
|
||||
|
||||
There are 3 stages in a file decoder's lifetime:
|
||||
There are 3 stages in a file codec's lifetime:
|
||||
|
||||
1. Setup: Pillow looks for a function in the decoder registry, falling
|
||||
back to a function named ``[decodername]_decoder`` on the internal
|
||||
core image object. That function is called with the ``args`` tuple
|
||||
from the ``tile`` setup in the ``_open`` method.
|
||||
1. Setup: Pillow looks for a function in the decoder or encoder registry,
|
||||
falling back to a function named ``[codecname]_decoder`` or
|
||||
``[codecname]_encoder`` on the internal core image object. That function is
|
||||
called with the ``args`` tuple from the ``tile``.
|
||||
|
||||
2. Decoding: The decoder's decode function is repeatedly called with
|
||||
chunks of image data.
|
||||
2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
|
||||
called with chunks of image data.
|
||||
|
||||
3. Cleanup: If the decoder has registered a cleanup function, it will
|
||||
be called at the end of the decoding process, even if there was an
|
||||
3. Cleanup: If the codec has registered a cleanup function, it will
|
||||
be called at the end of the transformation process, even if there was an
|
||||
exception raised.
|
||||
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
The current conventions are that the decoder setup function is named
|
||||
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The
|
||||
python binding for it is named ``[decodername]_decoder`` and is setup
|
||||
from within the ``_imaging.c`` file in the codecs section of the
|
||||
function array.
|
||||
The current conventions are that the codec setup function is named
|
||||
``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
|
||||
and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
|
||||
named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
|
||||
within the ``_imaging.c`` file in the codecs section of the function array.
|
||||
|
||||
The setup function needs to call ``PyImaging_DecoderNew`` and at the
|
||||
very least, set the ``decode`` function pointer. The fields of
|
||||
interest in this object are:
|
||||
The setup function needs to call ``PyImaging_DecoderNew`` or
|
||||
``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
|
||||
``encode`` function pointer. The fields of interest in this object are:
|
||||
|
||||
**decode**
|
||||
Function pointer to the decode function, which has access to
|
||||
``im``, ``state``, and the buffer of data to be added to the image.
|
||||
**decode**/**encode**
|
||||
Function pointer to the decode or encode function, which has access to
|
||||
``im``, ``state``, and the buffer of data to be transformed.
|
||||
|
||||
**cleanup**
|
||||
Function pointer to the cleanup function, has access to ``state``.
|
||||
|
@ -369,36 +373,34 @@ interest in this object are:
|
|||
|
||||
**state**
|
||||
An ImagingCodecStateInstance, will be set by Pillow. The ``context``
|
||||
member is an opaque struct that can be used by the decoder to store
|
||||
member is an opaque struct that can be used by the codec to store
|
||||
any format specific state or options.
|
||||
|
||||
**pulls_fd**
|
||||
**EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1,
|
||||
``state->fd`` will be a pointer to the Python file like object. The
|
||||
decoder may use the functions in ``codec_fd.c`` to read directly
|
||||
from the file like object rather than have the data pushed through a
|
||||
buffer. Note that this implementation may be refactored until this
|
||||
warning is removed.
|
||||
**pulls_fd**/**pushes_fd**
|
||||
If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1,
|
||||
``state->fd`` will be a pointer to the Python file like object. The codec may
|
||||
use the functions in ``codec_fd.c`` to read or write directly with the file
|
||||
like object rather than have the data pushed through a buffer.
|
||||
|
||||
.. versionadded:: 3.3.0
|
||||
|
||||
|
||||
Decoding
|
||||
--------
|
||||
Transforming
|
||||
------------
|
||||
|
||||
The decode function is called with the target (core) image, the
|
||||
decoder state structure, and a buffer of data to be decoded.
|
||||
The decode or encode function is called with the target (core) image, the codec
|
||||
state structure, and a buffer of data to be transformed.
|
||||
|
||||
**Experimental** -- If ``pulls_fd`` is set, then the decode function
|
||||
is called once, with an empty buffer. It is the decoder's
|
||||
responsibility to decode the entire tile in that one call. The rest of
|
||||
this section only applies if ``pulls_fd`` is not set.
|
||||
It is the codec's responsibility to pull as much data as possible out of the
|
||||
buffer and return the number of bytes consumed. The next call to the codec will
|
||||
include the previous unconsumed tail. The codec function will be called
|
||||
multiple times as the data processed.
|
||||
|
||||
It is the decoder's responsibility to pull as much data as possible
|
||||
out of the buffer and return the number of bytes consumed. The next
|
||||
call to the decoder will include the previous unconsumed tail. The
|
||||
decoder function will be called multiple times as the data is read
|
||||
from the file like object.
|
||||
Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
|
||||
encode function is called once, with an empty buffer. It is the codec's
|
||||
responsibility to transform the entire tile in that one call. Using this will
|
||||
provide a codec with more freedom, but that freedom may mean increased memory
|
||||
usage if the entire tile is held in memory at once by the codec.
|
||||
|
||||
If an error occurs, set ``state->errcode`` and return -1.
|
||||
|
||||
|
@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
|
|||
Cleanup
|
||||
-------
|
||||
|
||||
The cleanup function is called after the decoder returns a negative
|
||||
value, or if there is a read error from the file. This function should
|
||||
free any allocated memory and release any resources from external
|
||||
libraries.
|
||||
The cleanup function is called after the codec returns a negative
|
||||
value, or if there is an error. This function should free any allocated
|
||||
memory and release any resources from external libraries.
|
||||
|
||||
.. _file-codecs-py:
|
||||
|
||||
|
@ -425,11 +426,32 @@ They should be registered using :py:meth:`PIL.Image.register_decoder` and
|
|||
the file codecs, there are three stages in the lifetime of a
|
||||
Python-based file codec:
|
||||
|
||||
1. Setup: Pillow looks for the decoder in the registry, then
|
||||
1. Setup: Pillow looks for the codec in the decoder or encoder registry, then
|
||||
instantiates the class.
|
||||
|
||||
2. Transforming: The instance's ``decode`` method is repeatedly called with
|
||||
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
|
||||
called with the size of data to be output.
|
||||
|
||||
3. Cleanup: The instance's ``cleanup`` method is called.
|
||||
Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's
|
||||
``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode``
|
||||
will only be called once. In the decoder, ``self.fd`` can be used to access
|
||||
the file-like object. Using this will provide a codec with more freedom, but
|
||||
that freedom may mean increased memory usage if entire file is held in
|
||||
memory at once by the codec.
|
||||
|
||||
In ``decode``, once the data has been interpreted, ``set_as_raw`` can be
|
||||
used to populate the image.
|
||||
|
||||
3. Cleanup: The instance's ``cleanup`` method is called once the transformation
|
||||
is complete. This can be used to clean up any resources used by the codec.
|
||||
|
||||
If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you
|
||||
probably chose to perform any cleanup tasks at the end of ``decode`` or
|
||||
``encode``.
|
||||
|
||||
For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin
|
||||
<https://github.com/python-pillow/Pillow/blob/main/docs/example/DdsImagePlugin.py>`_.
|
||||
For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and
|
||||
:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin
|
||||
<https://github.com/python-pillow/Pillow/blob/main/src/PIL/BlpImagePlugin.py>`_
|
||||
|
|