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