Merge branch 'main' into codecov
|
@ -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
|
||||||
|
@ -31,24 +35,27 @@ python3 -m pip install -U pytest
|
||||||
python3 -m pip install -U pytest-cov
|
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
|
|
||||||
# TODO Remove condition when NumPy supports 3.11
|
|
||||||
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
|
|
||||||
|
|
||||||
# PyQt6 doesn't support PyPy3
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
python3 -m pip install numpy
|
||||||
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
|
# PyQt6 doesn't support PyPy3
|
||||||
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
|
sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 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
|
|
||||||
|
|
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/lint.yml
vendored
|
@ -2,6 +2,9 @@ name: Lint
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
|
|
4
.github/workflows/macos-install.sh
vendored
|
@ -12,11 +12,9 @@ python3 -m pip install -U pytest
|
||||||
python3 -m pip install -U pytest-cov
|
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
|
|
||||||
|
|
||||||
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
|
||||||
# TODO Remove condition when NumPy supports 3.11
|
python3 -m pip install numpy
|
||||||
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
|
||||||
|
|
6
.github/workflows/release-drafter.yml
vendored
|
@ -7,8 +7,14 @@ on:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
|
permissions:
|
||||||
|
contents: write # for release-drafter/release-drafter to create a github release
|
||||||
|
pull-requests: write # for release-drafter/release-drafter to add label to PR
|
||||||
if: github.repository == 'python-pillow/Pillow'
|
if: github.repository == 'python-pillow/Pillow'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
109
.github/workflows/test-cygwin.yml
vendored
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
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:
|
||||||
|
permissions:
|
||||||
|
contents: none
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Cygwin Test Successful
|
||||||
|
steps:
|
||||||
|
- name: Success
|
||||||
|
run: echo Cygwin Test Successful
|
18
.github/workflows/test-docker.yml
vendored
|
@ -2,6 +2,9 @@ name: Test Docker
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
|
@ -11,9 +14,9 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
docker: [
|
docker: [
|
||||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||||
ubuntu-20.04-focal-arm64v8,
|
ubuntu-22.04-jammy-arm64v8,
|
||||||
ubuntu-20.04-focal-ppc64le,
|
ubuntu-22.04-jammy-ppc64le,
|
||||||
ubuntu-20.04-focal-s390x,
|
ubuntu-22.04-jammy-s390x,
|
||||||
# Then run the remainder
|
# Then run the remainder
|
||||||
alpine,
|
alpine,
|
||||||
amazon-2-amd64,
|
amazon-2-amd64,
|
||||||
|
@ -24,6 +27,7 @@ jobs:
|
||||||
debian-10-buster-x86,
|
debian-10-buster-x86,
|
||||||
debian-11-bullseye-x86,
|
debian-11-bullseye-x86,
|
||||||
fedora-35-amd64,
|
fedora-35-amd64,
|
||||||
|
fedora-36-amd64,
|
||||||
gentoo,
|
gentoo,
|
||||||
ubuntu-18.04-bionic-amd64,
|
ubuntu-18.04-bionic-amd64,
|
||||||
ubuntu-20.04-focal-amd64,
|
ubuntu-20.04-focal-amd64,
|
||||||
|
@ -31,11 +35,11 @@ jobs:
|
||||||
]
|
]
|
||||||
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 }}
|
||||||
|
@ -81,6 +85,8 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
success:
|
success:
|
||||||
|
permissions:
|
||||||
|
contents: none
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Docker Test Successful
|
name: Docker Test Successful
|
||||||
|
|
5
.github/workflows/test-mingw.yml
vendored
|
@ -2,6 +2,9 @@ name: Test MinGW
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
@ -77,6 +80,8 @@ jobs:
|
||||||
CODECOV_NAME: ${{ matrix.name }}
|
CODECOV_NAME: ${{ matrix.name }}
|
||||||
|
|
||||||
success:
|
success:
|
||||||
|
permissions:
|
||||||
|
contents: none
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: MinGW Test Successful
|
name: MinGW Test Successful
|
||||||
|
|
5
.github/workflows/test-valgrind.yml
vendored
|
@ -13,6 +13,9 @@ on:
|
||||||
- "**.h"
|
- "**.h"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
|
@ -21,7 +24,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
docker: [
|
docker: [
|
||||||
ubuntu-20.04-focal-amd64-valgrind,
|
ubuntu-22.04-jammy-amd64-valgrind,
|
||||||
]
|
]
|
||||||
dockerTag: [main]
|
dockerTag: [main]
|
||||||
|
|
||||||
|
|
11
.github/workflows/test-windows.yml
vendored
|
@ -2,6 +2,9 @@ name: Test Windows
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
@ -41,10 +44,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
|
||||||
|
@ -189,6 +192,8 @@ jobs:
|
||||||
path: dist\*.whl
|
path: dist\*.whl
|
||||||
|
|
||||||
success:
|
success:
|
||||||
|
permissions:
|
||||||
|
contents: none
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Windows Test Successful
|
name: Windows Test Successful
|
||||||
|
|
5
.github/workflows/test.yml
vendored
|
@ -2,6 +2,9 @@ name: Test
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
|
@ -106,6 +109,8 @@ jobs:
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
success:
|
success:
|
||||||
|
permissions:
|
||||||
|
contents: none
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Test Successful
|
name: Test Successful
|
||||||
|
|
3
.github/workflows/tidelift.yml
vendored
|
@ -12,6 +12,9 @@ on:
|
||||||
- ".github/workflows/tidelift.yml"
|
- ".github/workflows/tidelift.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: github.repository_owner == 'python-pillow'
|
if: github.repository_owner == 'python-pillow'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.3.0
|
rev: 22.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: ["--target-version", "py37"]
|
args: ["--target-version", "py37"]
|
||||||
|
@ -19,13 +19,13 @@ repos:
|
||||||
- id: yesqa
|
- id: yesqa
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.1.13
|
rev: v1.3.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: 4.0.1
|
rev: 5.0.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||||
|
@ -37,10 +37,15 @@ repos:
|
||||||
- id: rst-backticks
|
- id: rst-backticks
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.1.0
|
rev: v4.3.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.1
|
||||||
|
hooks:
|
||||||
|
- id: sphinx-lint
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
autoupdate_schedule: quarterly
|
autoupdate_schedule: monthly
|
||||||
|
|
149
CHANGES.rst
|
@ -2,9 +2,144 @@
|
||||||
Changelog (Pillow)
|
Changelog (Pillow)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
9.2.0 (unreleased)
|
9.3.0 (unreleased)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Allow default ImageDraw font to be set #6484
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Save 1 mode PDF using CCITTFaxDecode filter #6470
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added support for RGBA PSD images #6481
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Parse orientation from XMP tag contents #6463
|
||||||
|
[bigcat88, radarhere]
|
||||||
|
|
||||||
|
- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457
|
||||||
|
[REDxEYE, radarhere]
|
||||||
|
|
||||||
|
- Do not clear GIF tile when checking number of frames #6455
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support saving multiple MPO frames #6444
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not double quote Pillow version for setuptools >= 60 #6450
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added ABGR BMP mask mode #6436
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed PSDraw rectangle #6429
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Raise ValueError if PNG sRGB chunk is truncated #6431
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Handle missing Python executable in ImageShow on macOS #6416
|
||||||
|
[bryant1410, radarhere]
|
||||||
|
|
||||||
|
9.2.0 (2022-07-01)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Deprecate ImageFont.getsize and related functions #6381
|
||||||
|
[nulano, radarhere]
|
||||||
|
|
||||||
|
- Fixed null check for fribidi_version_info in FriBiDi shim #6376
|
||||||
|
[nulano]
|
||||||
|
|
||||||
|
- Added GIF decompression bomb check #6402
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Handle PCF fonts files with less than 256 characters #6386
|
||||||
|
[dawidcrivelli, radarhere]
|
||||||
|
|
||||||
|
- Improved GIF optimize condition #6378
|
||||||
|
[raygard, radarhere]
|
||||||
|
|
||||||
|
- Reverted to __array_interface__ with the release of NumPy 1.23 #6394
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Pad PCX palette to 768 bytes when saving #6391
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed bug with rounding pixels to palette colors #6377
|
||||||
|
[btrekkie, radarhere]
|
||||||
|
|
||||||
|
- Use gnome-screenshot on Linux if available #6361
|
||||||
|
[radarhere, nulano]
|
||||||
|
|
||||||
|
- Fixed loading L mode BMP RLE8 images #6384
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed incorrect operator in ImageCms error #6370
|
||||||
|
[LostBenjamin, hugovk, radarhere]
|
||||||
|
|
||||||
|
- Limit FPX tile size to avoid extending outside image #6368
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added support for decoding plain PPM formats #5242
|
||||||
|
[Piolie, radarhere]
|
||||||
|
|
||||||
|
- Added apply_transparency() #6352
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed behaviour change from endian fix #6197
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Allow remapping P images with RGBA palettes #6350
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed drawing translucent 1px high polygons #6278
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Pad COLORMAP to 768 items when saving TIFF #6232
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fix P -> PA conversion #6337
|
||||||
|
[RedShy, radarhere]
|
||||||
|
|
||||||
|
- Once exif data is parsed, do not reload unless it changes #6335
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Only try to connect discontiguous corners at the end of edges #6303
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improve transparency handling when saving GIF images #6176
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not update GIF frame position until local image is found #6219
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Netscape GIF extension belongs after the global color table #6211
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Only write GIF comments at the beginning of the file #6300
|
||||||
|
[raygard, radarhere]
|
||||||
|
|
||||||
|
- Separate multiple GIF comment blocks with newlines #6294
|
||||||
|
[raygard, radarhere]
|
||||||
|
|
||||||
|
- Always use GIF89a for comments #6292
|
||||||
|
[raygard, radarhere]
|
||||||
|
|
||||||
|
- Ignore compression value from BMP info dictionary when saving as TIFF #6231
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- If font is file-like object, do not re-read from object to get variant #6234
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Raise ValueError when trying to access internal fp after close #6213
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support more affine expression forms in im.point() #6254
|
||||||
|
[benrg, radarhere]
|
||||||
|
|
||||||
|
- Populate Python palette in fromarray() #6283
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Raise ValueError if PNG chunks are truncated #6253
|
- Raise ValueError if PNG chunks are truncated #6253
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
@ -14,9 +149,6 @@ Changelog (Pillow)
|
||||||
- Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270
|
- Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
- Do not open images with zero or negative height #6269
|
|
||||||
[radarhere]
|
|
||||||
|
|
||||||
- Search pkgconf system libs/cflags #6138
|
- Search pkgconf system libs/cflags #6138
|
||||||
[jameshilliard, radarhere]
|
[jameshilliard, radarhere]
|
||||||
|
|
||||||
|
@ -47,6 +179,15 @@ Changelog (Pillow)
|
||||||
- Deprecated PhotoImage.paste() box parameter #6178
|
- Deprecated PhotoImage.paste() box parameter #6178
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
9.1.1 (2022-05-17)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- When reading past the end of a TGA scan line, reduce bytes left. CVE-2022-30595
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not open images with zero or negative height #6269
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
9.1.0 (2022-04-01)
|
9.1.0 (2022-04-01)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
2
Makefile
|
@ -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>
|
||||||
|
|
|
@ -24,7 +24,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
python3 -m twine check --strict dist/*
|
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||||
|
@ -61,7 +60,6 @@ Released as needed for security, installation or critical bug fixes.
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
python3 -m twine check --strict dist/*
|
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||||
|
@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
python3 -m twine check --strict dist/*
|
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||||
|
@ -99,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
## Binary Distributions
|
## Binary Distributions
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
* [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174.
|
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
||||||
* [ ] Download and extract tarball from `@cgohlke` and copy into `dist/`
|
and copy into `dist/`
|
||||||
|
|
||||||
### Mac and Linux
|
### Mac and Linux
|
||||||
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
||||||
|
|
|
@ -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
|
||||||
---------
|
---------
|
||||||
|
|
BIN
Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf
Normal file
BIN
Tests/images/ati1.dds
Normal file
BIN
Tests/images/ati1.png
Normal file
After Width: | Height: | Size: 969 B |
BIN
Tests/images/ati2.dds
Normal file
BIN
Tests/images/comment_after_last_frame.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/comment_after_only_frame.gif
Normal file
After Width: | Height: | Size: 54 B |
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/decompression_bomb_extents.gif
Normal file
After Width: | Height: | Size: 368 B |
BIN
Tests/images/duplicate_number_of_loops.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/hopper_16bit.pgm
Normal file
4
Tests/images/hopper_16bit_plain.pgm
Normal file
BIN
Tests/images/hopper_1bit.pbm
Normal file
14
Tests/images/hopper_1bit_plain.pbm
Normal file
BIN
Tests/images/hopper_8bit.pgm
Normal file
BIN
Tests/images/hopper_8bit.ppm
Normal file
4
Tests/images/hopper_8bit_plain.pgm
Normal file
BIN
Tests/images/hopper_8bit_plain.ppm
Normal file
BIN
Tests/images/hopper_rle8_greyscale.bmp
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
Tests/images/imagedraw_polygon_1px_high_translucent.png
Normal file
After Width: | Height: | Size: 76 B |
BIN
Tests/images/input_bw_one_band.fpx
Normal file
BIN
Tests/images/input_bw_one_band.png
Normal file
After Width: | Height: | Size: 477 B |
BIN
Tests/images/issue_6194.j2k
Normal file
BIN
Tests/images/multiple_comments.gif
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
Tests/images/rgb32bf-abgr.bmp
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
Tests/images/rgba.psd
Normal file
BIN
Tests/images/second_frame_comment.gif
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
Tests/images/tiny.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
Tests/images/xmp_tags_orientation_exiftool.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
|
@ -33,9 +33,9 @@ def fuzz_font(data):
|
||||||
# different font objects.
|
# different font objects.
|
||||||
return
|
return
|
||||||
|
|
||||||
font.getsize_multiline("ABC\nAaaa")
|
font.getbbox("ABC")
|
||||||
font.getmask("test text")
|
font.getmask("test text")
|
||||||
with Image.new(mode="RGBA", size=(200, 200)) as im:
|
with Image.new(mode="RGBA", size=(200, 200)) as im:
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2)
|
draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2)
|
||||||
draw.text((10, 10), "Test Text", font=font, fill="#000")
|
draw.text((10, 10), "Test Text", font=font, fill="#000")
|
||||||
|
|
|
@ -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"):
|
||||||
|
@ -62,6 +61,11 @@ class TestDecompressionBomb:
|
||||||
with Image.open("Tests/images/decompression_bomb.gif"):
|
with Image.open("Tests/images/decompression_bomb.gif"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_exception_gif_extents(self):
|
||||||
|
with Image.open("Tests/images/decompression_bomb_extents.gif") as im:
|
||||||
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
|
im.seek(1)
|
||||||
|
|
||||||
def test_exception_bmp(self):
|
def test_exception_bmp(self):
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
|
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
|
||||||
|
|
|
@ -325,8 +325,9 @@ def test_apng_syntax_errors():
|
||||||
pytest.warns(UserWarning, open)
|
pytest.warns(UserWarning, open)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_sequence_errors():
|
@pytest.mark.parametrize(
|
||||||
test_files = [
|
"test_file",
|
||||||
|
(
|
||||||
"sequence_start.png",
|
"sequence_start.png",
|
||||||
"sequence_gap.png",
|
"sequence_gap.png",
|
||||||
"sequence_repeat.png",
|
"sequence_repeat.png",
|
||||||
|
@ -334,12 +335,13 @@ def test_apng_sequence_errors():
|
||||||
"sequence_reorder.png",
|
"sequence_reorder.png",
|
||||||
"sequence_reorder_chunk.png",
|
"sequence_reorder_chunk.png",
|
||||||
"sequence_fdat_fctl.png",
|
"sequence_fdat_fctl.png",
|
||||||
]
|
),
|
||||||
for f in test_files:
|
)
|
||||||
with pytest.raises(SyntaxError):
|
def test_apng_sequence_errors(test_file):
|
||||||
with Image.open(f"Tests/images/apng/{f}") as im:
|
with pytest.raises(SyntaxError):
|
||||||
im.seek(im.n_frames - 1)
|
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
||||||
im.load()
|
im.seek(im.n_frames - 1)
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save(tmp_path):
|
def test_apng_save(tmp_path):
|
||||||
|
@ -637,6 +639,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_",
|
||||||
|
|
|
@ -129,11 +129,21 @@ 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")
|
||||||
|
|
||||||
|
# This test image has been manually hexedited
|
||||||
|
# to change the bitfield compression in the header from XBGR to ABGR
|
||||||
|
with Image.open("Tests/images/rgb32bf-abgr.bmp") as im:
|
||||||
|
assert_image_equal_tofile(
|
||||||
|
im.convert("RGB"), "Tests/images/bmp/q/rgb32bf-xbgr.bmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_rle8():
|
def test_rle8():
|
||||||
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
||||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im:
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
|
||||||
|
|
||||||
# This test image has been manually hexedited
|
# This test image has been manually hexedited
|
||||||
# to have rows with too much data
|
# to have rows with too much data
|
||||||
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
|
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import ContainerIO, Image
|
from PIL import ContainerIO, Image
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper
|
||||||
|
@ -59,89 +61,89 @@ def test_seek_mode_2():
|
||||||
assert container.tell() == 100
|
assert container.tell() == 100
|
||||||
|
|
||||||
|
|
||||||
def test_read_n0():
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
|
def test_read_n0(bytesmode):
|
||||||
# Arrange
|
# Arrange
|
||||||
for bytesmode in (True, False):
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
container.seek(81)
|
container.seek(81)
|
||||||
data = container.read()
|
data = container.read()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
if bytesmode:
|
if bytesmode:
|
||||||
data = data.decode()
|
data = data.decode()
|
||||||
assert data == "7\nThis is line 8\n"
|
assert data == "7\nThis is line 8\n"
|
||||||
|
|
||||||
|
|
||||||
def test_read_n():
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
|
def test_read_n(bytesmode):
|
||||||
# Arrange
|
# Arrange
|
||||||
for bytesmode in (True, False):
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
container.seek(81)
|
container.seek(81)
|
||||||
data = container.read(3)
|
data = container.read(3)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
if bytesmode:
|
if bytesmode:
|
||||||
data = data.decode()
|
data = data.decode()
|
||||||
assert data == "7\nT"
|
assert data == "7\nT"
|
||||||
|
|
||||||
|
|
||||||
def test_read_eof():
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
|
def test_read_eof(bytesmode):
|
||||||
# Arrange
|
# Arrange
|
||||||
for bytesmode in (True, False):
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
container.seek(100)
|
container.seek(100)
|
||||||
data = container.read()
|
data = container.read()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
if bytesmode:
|
if bytesmode:
|
||||||
data = data.decode()
|
data = data.decode()
|
||||||
assert data == ""
|
assert data == ""
|
||||||
|
|
||||||
|
|
||||||
def test_readline():
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
|
def test_readline(bytesmode):
|
||||||
# Arrange
|
# Arrange
|
||||||
for bytesmode in (True, False):
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
data = container.readline()
|
data = container.readline()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
if bytesmode:
|
if bytesmode:
|
||||||
data = data.decode()
|
data = data.decode()
|
||||||
assert data == "This is line 1\n"
|
assert data == "This is line 1\n"
|
||||||
|
|
||||||
|
|
||||||
def test_readlines():
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
|
def test_readlines(bytesmode):
|
||||||
# Arrange
|
# Arrange
|
||||||
for bytesmode in (True, False):
|
expected = [
|
||||||
expected = [
|
"This is line 1\n",
|
||||||
"This is line 1\n",
|
"This is line 2\n",
|
||||||
"This is line 2\n",
|
"This is line 3\n",
|
||||||
"This is line 3\n",
|
"This is line 4\n",
|
||||||
"This is line 4\n",
|
"This is line 5\n",
|
||||||
"This is line 5\n",
|
"This is line 6\n",
|
||||||
"This is line 6\n",
|
"This is line 7\n",
|
||||||
"This is line 7\n",
|
"This is line 8\n",
|
||||||
"This is line 8\n",
|
]
|
||||||
]
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
data = container.readlines()
|
data = container.readlines()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
if bytesmode:
|
if bytesmode:
|
||||||
data = [line.decode() for line in data]
|
data = [line.decode() for line in data]
|
||||||
assert data == expected
|
assert data == expected
|
||||||
|
|
|
@ -10,6 +10,8 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||||
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
|
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
|
||||||
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
|
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
|
||||||
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
|
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
|
||||||
|
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
|
||||||
|
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
|
||||||
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
|
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
|
||||||
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
||||||
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
||||||
|
@ -62,6 +64,32 @@ def test_sanity_dxt5():
|
||||||
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity_ati1():
|
||||||
|
"""Check ATI1 images can be opened"""
|
||||||
|
|
||||||
|
with Image.open(TEST_FILE_ATI1) as im:
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
assert im.format == "DDS"
|
||||||
|
assert im.mode == "L"
|
||||||
|
assert im.size == (64, 64)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity_ati2():
|
||||||
|
"""Check ATI2 images can be opened"""
|
||||||
|
|
||||||
|
with Image.open(TEST_FILE_ATI2) as im:
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
assert im.format == "DDS"
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
assert im.size == (256, 256)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("image_path", "expected_path"),
|
("image_path", "expected_path"),
|
||||||
(
|
(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -2,11 +2,22 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from .helper import assert_image_equal_tofile
|
||||||
|
|
||||||
FpxImagePlugin = pytest.importorskip(
|
FpxImagePlugin = pytest.importorskip(
|
||||||
"PIL.FpxImagePlugin", reason="olefile not installed"
|
"PIL.FpxImagePlugin", reason="olefile not installed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity():
|
||||||
|
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||||
|
assert im.mode == "L"
|
||||||
|
assert im.size == (70, 46)
|
||||||
|
assert im.format == "FPX"
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
# Test an invalid OLE file
|
# Test an invalid OLE file
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -145,6 +158,9 @@ def test_optimize_correctness():
|
||||||
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
|
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
|
||||||
|
|
||||||
# These do optimize the palette
|
# These do optimize the palette
|
||||||
|
check(256, 511, 256)
|
||||||
|
check(255, 511, 255)
|
||||||
|
check(129, 511, 129)
|
||||||
check(128, 511, 128)
|
check(128, 511, 128)
|
||||||
check(64, 511, 64)
|
check(64, 511, 64)
|
||||||
check(4, 511, 4)
|
check(4, 511, 4)
|
||||||
|
@ -154,11 +170,6 @@ def test_optimize_correctness():
|
||||||
check(64, 513, 256)
|
check(64, 513, 256)
|
||||||
check(4, 513, 256)
|
check(4, 513, 256)
|
||||||
|
|
||||||
# Other limits that don't optimize the palette
|
|
||||||
check(129, 511, 256)
|
|
||||||
check(255, 511, 256)
|
|
||||||
check(256, 511, 256)
|
|
||||||
|
|
||||||
|
|
||||||
def test_optimize_full_l():
|
def test_optimize_full_l():
|
||||||
im = Image.frombytes("L", (16, 16), bytes(range(256)))
|
im = Image.frombytes("L", (16, 16), bytes(range(256)))
|
||||||
|
@ -167,6 +178,19 @@ def test_optimize_full_l():
|
||||||
assert im.mode == "L"
|
assert im.mode == "L"
|
||||||
|
|
||||||
|
|
||||||
|
def test_optimize_if_palette_can_be_reduced_by_half():
|
||||||
|
with Image.open("Tests/images/test.colors.gif") as im:
|
||||||
|
# Reduce dimensions because original is too big for _get_optimize()
|
||||||
|
im = im.resize((591, 443))
|
||||||
|
im_rgb = im.convert("RGB")
|
||||||
|
|
||||||
|
for (optimize, colors) in ((False, 256), (True, 8)):
|
||||||
|
out = BytesIO()
|
||||||
|
im_rgb.save(out, "GIF", optimize=optimize)
|
||||||
|
with Image.open(out) as reloaded:
|
||||||
|
assert len(reloaded.palette.palette) // 3 == colors
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip(tmp_path):
|
def test_roundtrip(tmp_path):
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
@ -341,16 +365,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():
|
||||||
|
@ -368,6 +399,11 @@ def test_no_change():
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
|
||||||
|
expected = Image.new("P", (1, 1))
|
||||||
|
assert not im.is_animated
|
||||||
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror():
|
def test_eoferror():
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
|
@ -619,7 +655,8 @@ def test_dispose2_background(tmp_path):
|
||||||
assert im.getpixel((0, 0)) == (255, 0, 0)
|
assert im.getpixel((0, 0)) == (255, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_transparency_in_second_frame():
|
def test_transparency_in_second_frame(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||||
assert im.info["transparency"] == 0
|
assert im.info["transparency"] == 0
|
||||||
|
|
||||||
|
@ -629,6 +666,14 @@ def test_transparency_in_second_frame():
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
|
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
|
||||||
|
|
||||||
|
im.save(out, save_all=True)
|
||||||
|
|
||||||
|
with Image.open(out) as reread:
|
||||||
|
reread.seek(reread.tell() + 1)
|
||||||
|
assert_image_equal_tofile(
|
||||||
|
reread, "Tests/images/different_transparency_merged.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_no_transparency_in_second_frame():
|
def test_no_transparency_in_second_frame():
|
||||||
with Image.open("Tests/images/iss634.gif") as img:
|
with Image.open("Tests/images/iss634.gif") as img:
|
||||||
|
@ -640,6 +685,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
|
||||||
|
|
||||||
|
@ -759,9 +820,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")
|
||||||
|
@ -794,6 +862,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")
|
||||||
|
@ -804,15 +875,67 @@ 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")
|
||||||
|
|
||||||
|
@ -875,8 +998,8 @@ def test_append_images(tmp_path):
|
||||||
def test_transparent_optimize(tmp_path):
|
def test_transparent_optimize(tmp_path):
|
||||||
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
||||||
# transparency.
|
# transparency.
|
||||||
# Need a palette that isn't using the 0 color, and one that's > 128 items where the
|
# Need a palette that isn't using the 0 color,
|
||||||
# transparent color is actually the top palette entry to trigger the bug.
|
# where the transparent color is actually the top palette entry to trigger the bug.
|
||||||
|
|
||||||
data = bytes(range(1, 254))
|
data = bytes(range(1, 254))
|
||||||
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
||||||
|
@ -886,10 +1009,10 @@ def test_transparent_optimize(tmp_path):
|
||||||
im.putpalette(palette)
|
im.putpalette(palette)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im.save(out, transparency=253)
|
im.save(out, transparency=im.getpixel((252, 0)))
|
||||||
with Image.open(out) as reloaded:
|
|
||||||
|
|
||||||
assert reloaded.info["transparency"] == 253
|
with Image.open(out) as reloaded:
|
||||||
|
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
|
||||||
|
|
||||||
|
|
||||||
def test_rgb_transparency(tmp_path):
|
def test_rgb_transparency(tmp_path):
|
||||||
|
|
|
@ -78,15 +78,12 @@ def test_eoferror():
|
||||||
im.seek(n_frames - 1)
|
im.seek(n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip(tmp_path):
|
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
||||||
def roundtrip(mode):
|
def test_roundtrip(mode, tmp_path):
|
||||||
out = str(tmp_path / "temp.im")
|
out = str(tmp_path / "temp.im")
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(out)
|
im.save(out)
|
||||||
assert_image_equal_tofile(im, out)
|
assert_image_equal_tofile(im, out)
|
||||||
|
|
||||||
for mode in ["RGB", "P", "PA"]:
|
|
||||||
roundtrip(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_unsupported_mode(tmp_path):
|
def test_save_unsupported_mode(tmp_path):
|
||||||
|
|
|
@ -298,6 +298,11 @@ def test_16bit_jp2_roundtrips():
|
||||||
assert_image_equal(im, jp2)
|
assert_image_equal(im, jp2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_issue_6194():
|
||||||
|
with Image.open("Tests/images/issue_6194.j2k") as im:
|
||||||
|
assert im.getpixel((5, 5)) == 31
|
||||||
|
|
||||||
|
|
||||||
def test_unbound_local():
|
def test_unbound_local():
|
||||||
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -135,50 +135,50 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||||
|
|
||||||
def test_write_metadata(self, tmp_path):
|
@pytest.mark.parametrize("legacy_api", (False, True))
|
||||||
|
def test_write_metadata(self, legacy_api, tmp_path):
|
||||||
"""Test metadata writing through libtiff"""
|
"""Test metadata writing through libtiff"""
|
||||||
for legacy_api in [False, True]:
|
f = str(tmp_path / "temp.tiff")
|
||||||
f = str(tmp_path / "temp.tiff")
|
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
img.save(f, tiffinfo=img.tag)
|
||||||
img.save(f, tiffinfo=img.tag)
|
|
||||||
|
|
||||||
if legacy_api:
|
if legacy_api:
|
||||||
original = img.tag.named()
|
original = img.tag.named()
|
||||||
else:
|
else:
|
||||||
original = img.tag_v2.named()
|
original = img.tag_v2.named()
|
||||||
|
|
||||||
# PhotometricInterpretation is set from SAVE_INFO,
|
# PhotometricInterpretation is set from SAVE_INFO,
|
||||||
# not the original image.
|
# not the original image.
|
||||||
ignored = [
|
ignored = [
|
||||||
"StripByteCounts",
|
"StripByteCounts",
|
||||||
"RowsPerStrip",
|
"RowsPerStrip",
|
||||||
"PageNumber",
|
"PageNumber",
|
||||||
"PhotometricInterpretation",
|
"PhotometricInterpretation",
|
||||||
]
|
]
|
||||||
|
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
if legacy_api:
|
if legacy_api:
|
||||||
reloaded = loaded.tag.named()
|
reloaded = loaded.tag.named()
|
||||||
else:
|
else:
|
||||||
reloaded = loaded.tag_v2.named()
|
reloaded = loaded.tag_v2.named()
|
||||||
|
|
||||||
for tag, value in itertools.chain(reloaded.items(), original.items()):
|
for tag, value in itertools.chain(reloaded.items(), original.items()):
|
||||||
if tag not in ignored:
|
if tag not in ignored:
|
||||||
val = original[tag]
|
val = original[tag]
|
||||||
if tag.endswith("Resolution"):
|
if tag.endswith("Resolution"):
|
||||||
if legacy_api:
|
if legacy_api:
|
||||||
assert val[0][0] / val[0][1] == (
|
assert val[0][0] / val[0][1] == (
|
||||||
4294967295 / 113653537
|
4294967295 / 113653537
|
||||||
), f"{tag} didn't roundtrip"
|
), f"{tag} didn't roundtrip"
|
||||||
else:
|
|
||||||
assert val == 37.79000115940079, f"{tag} didn't roundtrip"
|
|
||||||
else:
|
else:
|
||||||
assert val == value, f"{tag} didn't roundtrip"
|
assert val == 37.79000115940079, f"{tag} didn't roundtrip"
|
||||||
|
else:
|
||||||
|
assert val == value, f"{tag} didn't roundtrip"
|
||||||
|
|
||||||
# https://github.com/python-pillow/Pillow/issues/1561
|
# https://github.com/python-pillow/Pillow/issues/1561
|
||||||
requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"]
|
requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"]
|
||||||
for field in requested_fields:
|
for field in requested_fields:
|
||||||
assert field in reloaded, f"{field} not in metadata"
|
assert field in reloaded, f"{field} not in metadata"
|
||||||
|
|
||||||
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
|
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
|
||||||
def test_additional_metadata(self, tmp_path):
|
def test_additional_metadata(self, tmp_path):
|
||||||
|
@ -497,8 +497,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im.save(out, compression="tiff_adobe_deflate")
|
im.save(out, compression="tiff_adobe_deflate")
|
||||||
assert_image_equal_tofile(im, out)
|
assert_image_equal_tofile(im, out)
|
||||||
|
|
||||||
def test_palette_save(self, tmp_path):
|
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||||
im = hopper("P")
|
def test_palette_save(self, im, tmp_path):
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
|
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||||
|
@ -856,7 +856,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_strip_ycbcr_jpeg_2x2_sampling(self):
|
def test_strip_ycbcr_jpeg_2x2_sampling(self):
|
||||||
infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif"
|
infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5)
|
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2)
|
||||||
|
|
||||||
@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"
|
||||||
|
@ -864,7 +864,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_strip_ycbcr_jpeg_1x1_sampling(self):
|
def test_strip_ycbcr_jpeg_1x1_sampling(self):
|
||||||
infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif"
|
infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/flower2.jpg")
|
assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01)
|
||||||
|
|
||||||
def test_tiled_cmyk_jpeg(self):
|
def test_tiled_cmyk_jpeg(self):
|
||||||
infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif"
|
infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif"
|
||||||
|
@ -877,7 +877,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_tiled_ycbcr_jpeg_1x1_sampling(self):
|
def test_tiled_ycbcr_jpeg_1x1_sampling(self):
|
||||||
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif"
|
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/flower2.jpg")
|
assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01)
|
||||||
|
|
||||||
@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"
|
||||||
|
@ -885,7 +885,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_tiled_ycbcr_jpeg_2x2_sampling(self):
|
def test_tiled_ycbcr_jpeg_2x2_sampling(self):
|
||||||
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif"
|
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5)
|
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5)
|
||||||
|
|
||||||
def test_strip_planar_rgb(self):
|
def test_strip_planar_rgb(self):
|
||||||
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
|
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
|
||||||
|
@ -1011,14 +1011,18 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
# Assert that there are multiple strips
|
# Assert that there are multiple strips
|
||||||
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
||||||
|
|
||||||
def test_save_single_strip(self, tmp_path):
|
@pytest.mark.parametrize("argument", (True, False))
|
||||||
|
def test_save_single_strip(self, argument, tmp_path):
|
||||||
im = hopper("RGB").resize((256, 256))
|
im = hopper("RGB").resize((256, 256))
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
|
|
||||||
TiffImagePlugin.STRIP_SIZE = 2**18
|
if not argument:
|
||||||
|
TiffImagePlugin.STRIP_SIZE = 2**18
|
||||||
try:
|
try:
|
||||||
|
arguments = {"compression": "tiff_adobe_deflate"}
|
||||||
im.save(out, compression="tiff_adobe_deflate")
|
if argument:
|
||||||
|
arguments["strip_size"] = 2**18
|
||||||
|
im.save(out, **arguments)
|
||||||
|
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
||||||
|
|
|
@ -5,15 +5,19 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import assert_image_similar, is_pypy, skip_unless_feature
|
from .helper import (
|
||||||
|
assert_image_equal,
|
||||||
|
assert_image_similar,
|
||||||
|
is_pypy,
|
||||||
|
skip_unless_feature,
|
||||||
|
)
|
||||||
|
|
||||||
test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
||||||
|
|
||||||
pytestmark = skip_unless_feature("jpg")
|
pytestmark = skip_unless_feature("jpg")
|
||||||
|
|
||||||
|
|
||||||
def frame_roundtrip(im, **options):
|
def roundtrip(im, **options):
|
||||||
# Note that for now, there is no MPO saving functionality
|
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, "MPO", **options)
|
im.save(out, "MPO", **options)
|
||||||
test_bytes = out.tell()
|
test_bytes = out.tell()
|
||||||
|
@ -23,13 +27,13 @@ def frame_roundtrip(im, **options):
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
for test_file in test_files:
|
def test_sanity(test_file):
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
im.load()
|
im.load()
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
assert im.size == (640, 480)
|
assert im.size == (640, 480)
|
||||||
assert im.format == "MPO"
|
assert im.format == "MPO"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||||
|
@ -48,32 +52,39 @@ 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:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_app():
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
for test_file in test_files:
|
def test_app(test_file):
|
||||||
# Test APP/COM reader (@PIL135)
|
# Test APP/COM reader (@PIL135)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.applist[0][0] == "APP1"
|
assert im.applist[0][0] == "APP1"
|
||||||
assert im.applist[1][0] == "APP2"
|
assert im.applist[1][0] == "APP2"
|
||||||
assert (
|
assert (
|
||||||
im.applist[1][1][:16]
|
im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
|
||||||
== b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
|
)
|
||||||
)
|
assert len(im.applist) == 2
|
||||||
assert len(im.applist) == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_exif():
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
for test_file in test_files:
|
def test_exif(test_file):
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
info = im._getexif()
|
info = im._getexif()
|
||||||
assert info[272] == "Nintendo 3DS"
|
assert info[272] == "Nintendo 3DS"
|
||||||
assert info[296] == 2
|
assert info[296] == 2
|
||||||
assert info[34665] == 188
|
assert info[34665] == 188
|
||||||
|
|
||||||
|
|
||||||
def test_frame_size():
|
def test_frame_size():
|
||||||
|
@ -116,12 +127,21 @@ def test_parallax():
|
||||||
assert exif.get_ifd(0x927C)[0xB211] == -3.125
|
assert exif.get_ifd(0x927C)[0xB211] == -3.125
|
||||||
|
|
||||||
|
|
||||||
def test_mp():
|
def test_reload_exif_after_seek():
|
||||||
for test_file in test_files:
|
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||||
with Image.open(test_file) as im:
|
exif = im.getexif()
|
||||||
mpinfo = im._getmp()
|
del exif[296]
|
||||||
assert mpinfo[45056] == b"0100"
|
|
||||||
assert mpinfo[45057] == 2
|
im.seek(1)
|
||||||
|
assert 296 in exif
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
|
def test_mp(test_file):
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
mpinfo = im._getmp()
|
||||||
|
assert mpinfo[45056] == b"0100"
|
||||||
|
assert mpinfo[45057] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_mp_offset():
|
def test_mp_offset():
|
||||||
|
@ -141,48 +161,48 @@ def test_mp_no_data():
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
|
||||||
|
|
||||||
def test_mp_attribute():
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
for test_file in test_files:
|
def test_mp_attribute(test_file):
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
mpinfo = im._getmp()
|
mpinfo = im._getmp()
|
||||||
frame_number = 0
|
frame_number = 0
|
||||||
for mpentry in mpinfo[0xB002]:
|
for mpentry in mpinfo[0xB002]:
|
||||||
mpattr = mpentry["Attribute"]
|
mpattr = mpentry["Attribute"]
|
||||||
if frame_number:
|
if frame_number:
|
||||||
assert not mpattr["RepresentativeImageFlag"]
|
assert not mpattr["RepresentativeImageFlag"]
|
||||||
else:
|
else:
|
||||||
assert mpattr["RepresentativeImageFlag"]
|
assert mpattr["RepresentativeImageFlag"]
|
||||||
assert not mpattr["DependentParentImageFlag"]
|
assert not mpattr["DependentParentImageFlag"]
|
||||||
assert not mpattr["DependentChildImageFlag"]
|
assert not mpattr["DependentChildImageFlag"]
|
||||||
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
|
||||||
frame_number += 1
|
frame_number += 1
|
||||||
|
|
||||||
|
|
||||||
def test_seek():
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
for test_file in test_files:
|
def test_seek(test_file):
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.tell() == 0
|
assert im.tell() == 0
|
||||||
# prior to first image raises an error, both blatant and borderline
|
# prior to first image raises an error, both blatant and borderline
|
||||||
with pytest.raises(EOFError):
|
with pytest.raises(EOFError):
|
||||||
im.seek(-1)
|
im.seek(-1)
|
||||||
with pytest.raises(EOFError):
|
with pytest.raises(EOFError):
|
||||||
im.seek(-523)
|
im.seek(-523)
|
||||||
# after the final image raises an error,
|
# after the final image raises an error,
|
||||||
# both blatant and borderline
|
# both blatant and borderline
|
||||||
with pytest.raises(EOFError):
|
with pytest.raises(EOFError):
|
||||||
im.seek(2)
|
im.seek(2)
|
||||||
with pytest.raises(EOFError):
|
with pytest.raises(EOFError):
|
||||||
im.seek(523)
|
im.seek(523)
|
||||||
# bad calls shouldn't change the frame
|
# bad calls shouldn't change the frame
|
||||||
assert im.tell() == 0
|
assert im.tell() == 0
|
||||||
# this one will work
|
# this one will work
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.tell() == 1
|
assert im.tell() == 1
|
||||||
# and this one, too
|
# and this one, too
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
assert im.tell() == 0
|
assert im.tell() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_n_frames():
|
def test_n_frames():
|
||||||
|
@ -204,29 +224,54 @@ def test_eoferror():
|
||||||
im.seek(n_frames - 1)
|
im.seek(n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
def test_image_grab():
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
|
def test_image_grab(test_file):
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
assert im.tell() == 0
|
||||||
|
im0 = im.tobytes()
|
||||||
|
im.seek(1)
|
||||||
|
assert im.tell() == 1
|
||||||
|
im1 = im.tobytes()
|
||||||
|
im.seek(0)
|
||||||
|
assert im.tell() == 0
|
||||||
|
im02 = im.tobytes()
|
||||||
|
assert im0 == im02
|
||||||
|
assert im0 != im1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("test_file", test_files)
|
||||||
|
def test_save(test_file):
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
assert im.tell() == 0
|
||||||
|
jpg0 = roundtrip(im)
|
||||||
|
assert_image_similar(im, jpg0, 30)
|
||||||
|
im.seek(1)
|
||||||
|
assert im.tell() == 1
|
||||||
|
jpg1 = roundtrip(im)
|
||||||
|
assert_image_similar(im, jpg1, 30)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_all():
|
||||||
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:
|
||||||
assert im.tell() == 0
|
im_reloaded = roundtrip(im, save_all=True)
|
||||||
im0 = im.tobytes()
|
|
||||||
im.seek(1)
|
|
||||||
assert im.tell() == 1
|
|
||||||
im1 = im.tobytes()
|
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
assert im.tell() == 0
|
assert_image_similar(im, im_reloaded, 30)
|
||||||
im02 = im.tobytes()
|
|
||||||
assert im0 == im02
|
|
||||||
assert im0 != im1
|
|
||||||
|
|
||||||
|
|
||||||
def test_save():
|
|
||||||
# Note that only individual frames can be saved at present
|
|
||||||
for test_file in test_files:
|
|
||||||
with Image.open(test_file) as im:
|
|
||||||
assert im.tell() == 0
|
|
||||||
jpg0 = frame_roundtrip(im)
|
|
||||||
assert_image_similar(im, jpg0, 30)
|
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.tell() == 1
|
im_reloaded.seek(1)
|
||||||
jpg1 = frame_roundtrip(im)
|
assert_image_similar(im, im_reloaded, 30)
|
||||||
assert_image_similar(im, jpg1, 30)
|
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
im2 = Image.new("RGB", (1, 1), "#f00")
|
||||||
|
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
|
assert_image_equal(im, im_reloaded)
|
||||||
|
|
||||||
|
im_reloaded.seek(1)
|
||||||
|
assert_image_similar(im2, im_reloaded, 1)
|
||||||
|
|
||||||
|
# Test that a single frame image will not be saved as an MPO
|
||||||
|
jpg = roundtrip(im, save_all=True)
|
||||||
|
assert "mp" not in jpg.info
|
||||||
|
|
|
@ -20,6 +20,11 @@ def test_sanity(tmp_path):
|
||||||
for mode in ("1", "L", "P", "RGB"):
|
for mode in ("1", "L", "P", "RGB"):
|
||||||
_roundtrip(tmp_path, hopper(mode))
|
_roundtrip(tmp_path, hopper(mode))
|
||||||
|
|
||||||
|
# Test a palette with less than 256 colors
|
||||||
|
im = Image.new("P", (1, 1))
|
||||||
|
im.putpalette((255, 0, 0))
|
||||||
|
_roundtrip(tmp_path, im)
|
||||||
|
|
||||||
# Test an unsupported mode
|
# Test an unsupported mode
|
||||||
f = str(tmp_path / "temp.pcx")
|
f = str(tmp_path / "temp.pcx")
|
||||||
im = hopper("RGBA")
|
im = hopper("RGBA")
|
||||||
|
|
|
@ -37,13 +37,14 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
|
||||||
return outfile
|
return outfile
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.valgrind_known_error(reason="Temporary skip")
|
||||||
def test_monochrome(tmp_path):
|
def test_monochrome(tmp_path):
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = "1"
|
mode = "1"
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
outfile = helper_save_as_pdf(tmp_path, mode)
|
outfile = helper_save_as_pdf(tmp_path, mode)
|
||||||
assert os.path.getsize(outfile) < 15000
|
assert os.path.getsize(outfile) < 5000
|
||||||
|
|
||||||
|
|
||||||
def test_greyscale(tmp_path):
|
def test_greyscale(tmp_path):
|
||||||
|
|
|
@ -635,7 +635,9 @@ 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"))
|
@pytest.mark.parametrize(
|
||||||
|
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
|
||||||
|
)
|
||||||
def test_truncated_chunks(self, cid):
|
def test_truncated_chunks(self, cid):
|
||||||
fp = BytesIO()
|
fp = BytesIO()
|
||||||
with PngImagePlugin.PngStream(fp) as png:
|
with PngImagePlugin.PngStream(fp) as png:
|
||||||
|
|
|
@ -3,7 +3,7 @@ from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, PpmImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
|
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
|
||||||
|
|
||||||
|
@ -22,6 +22,21 @@ def test_sanity():
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"data, mode, pixels",
|
"data, mode, pixels",
|
||||||
(
|
(
|
||||||
|
(b"P2 3 1 4 0 2 4", "L", (0, 128, 255)),
|
||||||
|
(b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)),
|
||||||
|
# P3 with maxval < 255
|
||||||
|
(
|
||||||
|
b"P3 3 1 17 0 1 2 8 9 10 15 16 17",
|
||||||
|
"RGB",
|
||||||
|
((0, 15, 30), (120, 135, 150), (225, 240, 255)),
|
||||||
|
),
|
||||||
|
# P3 with maxval > 255
|
||||||
|
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
||||||
|
(
|
||||||
|
b"P3 3 1 257 0 1 2 128 129 130 256 257 257",
|
||||||
|
"RGB",
|
||||||
|
((0, 1, 2), (127, 128, 129), (254, 255, 255)),
|
||||||
|
),
|
||||||
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
||||||
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
||||||
# P6 with maxval < 255
|
# P6 with maxval < 255
|
||||||
|
@ -35,7 +50,6 @@ def test_sanity():
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
# P6 with maxval > 255
|
# P6 with maxval > 255
|
||||||
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
|
||||||
(
|
(
|
||||||
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
||||||
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
||||||
|
@ -85,14 +99,111 @@ def test_pnm(tmp_path):
|
||||||
assert_image_equal_tofile(im, f)
|
assert_image_equal_tofile(im, f)
|
||||||
|
|
||||||
|
|
||||||
def test_magic(tmp_path):
|
@pytest.mark.parametrize(
|
||||||
|
"plain_path, raw_path",
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"Tests/images/hopper_1bit_plain.pbm", # P1
|
||||||
|
"Tests/images/hopper_1bit.pbm", # P4
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Tests/images/hopper_8bit_plain.pgm", # P2
|
||||||
|
"Tests/images/hopper_8bit.pgm", # P5
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Tests/images/hopper_8bit_plain.ppm", # P3
|
||||||
|
"Tests/images/hopper_8bit.ppm", # P6
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_plain(plain_path, raw_path):
|
||||||
|
with Image.open(plain_path) as im:
|
||||||
|
assert_image_equal_tofile(im, raw_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_16bit_plain_pgm():
|
||||||
|
# P2 with maxval 2 ** 16 - 1
|
||||||
|
with Image.open("Tests/images/hopper_16bit_plain.pgm") as im:
|
||||||
|
assert im.mode == "I"
|
||||||
|
assert im.size == (128, 128)
|
||||||
|
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||||
|
|
||||||
|
# P5 with maxval 2 ** 16 - 1
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"header, data, comment_count",
|
||||||
|
(
|
||||||
|
(b"P1\n2 2", b"1010", 10**6),
|
||||||
|
(b"P2\n3 1\n4", b"0 2 4", 1),
|
||||||
|
(b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_plain_data_with_comment(tmp_path, header, data, comment_count):
|
||||||
|
path1 = str(tmp_path / "temp1.ppm")
|
||||||
|
path2 = str(tmp_path / "temp2.ppm")
|
||||||
|
comment = b"# comment" * comment_count
|
||||||
|
with open(path1, "wb") as f1, open(path2, "wb") as f2:
|
||||||
|
f1.write(header + b"\n\n" + data)
|
||||||
|
f2.write(header + b"\n" + comment + b"\n" + data + comment)
|
||||||
|
|
||||||
|
with Image.open(path1) as im:
|
||||||
|
assert_image_equal_tofile(im, path2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
|
||||||
|
def test_plain_truncated_data(tmp_path, data):
|
||||||
path = str(tmp_path / "temp.ppm")
|
path = str(tmp_path / "temp.ppm")
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"PyInvalid")
|
f.write(data)
|
||||||
|
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with Image.open(path) as im:
|
||||||
with Image.open(path):
|
with pytest.raises(ValueError):
|
||||||
pass
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
|
||||||
|
def test_plain_invalid_data(tmp_path, data):
|
||||||
|
path = str(tmp_path / "temp.ppm")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
with Image.open(path) as im:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"data",
|
||||||
|
(
|
||||||
|
b"P3\n128 128\n255\n012345678910", # half token too long
|
||||||
|
b"P3\n128 128\n255\n012345678910 0", # token too long
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_plain_ppm_token_too_long(tmp_path, data):
|
||||||
|
path = str(tmp_path / "temp.ppm")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
with Image.open(path) as im:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_plain_ppm_value_too_large(tmp_path):
|
||||||
|
path = str(tmp_path / "temp.ppm")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(b"P3\n128 128\n255\n256")
|
||||||
|
|
||||||
|
with Image.open(path) as im:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_magic():
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid"))
|
||||||
|
|
||||||
|
|
||||||
def test_header_with_comments(tmp_path):
|
def test_header_with_comments(tmp_path):
|
||||||
|
@ -114,7 +225,7 @@ def test_non_integer_token(tmp_path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_token_too_long(tmp_path):
|
def test_header_token_too_long(tmp_path):
|
||||||
path = str(tmp_path / "temp.ppm")
|
path = str(tmp_path / "temp.ppm")
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"P6\n 01234567890")
|
f.write(b"P6\n 01234567890")
|
||||||
|
|
|
@ -4,7 +4,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, PsdImagePlugin
|
from PIL import Image, PsdImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_similar, hopper, is_pypy
|
from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
|
||||||
|
|
||||||
test_file = "Tests/images/hopper.psd"
|
test_file = "Tests/images/hopper.psd"
|
||||||
|
|
||||||
|
@ -107,6 +107,11 @@ def test_open_after_exclusive_load():
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rgba():
|
||||||
|
with Image.open("Tests/images/rgba.psd") as im:
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
|
||||||
|
|
||||||
|
|
||||||
def test_icc_profile():
|
def test_icc_profile():
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert "icc_profile" in im.info
|
assert "icc_profile" in im.info
|
||||||
|
|
|
@ -18,51 +18,48 @@ _ORIGINS = ("tl", "bl")
|
||||||
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
||||||
|
|
||||||
|
|
||||||
def test_sanity(tmp_path):
|
@pytest.mark.parametrize("mode", _MODES)
|
||||||
for mode in _MODES:
|
def test_sanity(mode, tmp_path):
|
||||||
|
def roundtrip(original_im):
|
||||||
|
out = str(tmp_path / "temp.tga")
|
||||||
|
|
||||||
def roundtrip(original_im):
|
original_im.save(out, rle=rle)
|
||||||
out = str(tmp_path / "temp.tga")
|
with Image.open(out) as saved_im:
|
||||||
|
if rle:
|
||||||
|
assert saved_im.info["compression"] == original_im.info["compression"]
|
||||||
|
assert saved_im.info["orientation"] == original_im.info["orientation"]
|
||||||
|
if mode == "P":
|
||||||
|
assert saved_im.getpalette() == original_im.getpalette()
|
||||||
|
|
||||||
original_im.save(out, rle=rle)
|
assert_image_equal(saved_im, original_im)
|
||||||
with Image.open(out) as saved_im:
|
|
||||||
if rle:
|
png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png"))
|
||||||
|
|
||||||
|
for png_path in png_paths:
|
||||||
|
with Image.open(png_path) as reference_im:
|
||||||
|
assert reference_im.mode == mode
|
||||||
|
|
||||||
|
path_no_ext = os.path.splitext(png_path)[0]
|
||||||
|
for origin, rle in product(_ORIGINS, (True, False)):
|
||||||
|
tga_path = "{}_{}_{}.tga".format(
|
||||||
|
path_no_ext, origin, "rle" if rle else "raw"
|
||||||
|
)
|
||||||
|
|
||||||
|
with Image.open(tga_path) as original_im:
|
||||||
|
assert original_im.format == "TGA"
|
||||||
|
assert original_im.get_format_mimetype() == "image/x-tga"
|
||||||
|
if rle:
|
||||||
|
assert original_im.info["compression"] == "tga_rle"
|
||||||
assert (
|
assert (
|
||||||
saved_im.info["compression"] == original_im.info["compression"]
|
original_im.info["orientation"]
|
||||||
|
== _ORIGIN_TO_ORIENTATION[origin]
|
||||||
)
|
)
|
||||||
assert saved_im.info["orientation"] == original_im.info["orientation"]
|
if mode == "P":
|
||||||
if mode == "P":
|
assert original_im.getpalette() == reference_im.getpalette()
|
||||||
assert saved_im.getpalette() == original_im.getpalette()
|
|
||||||
|
|
||||||
assert_image_equal(saved_im, original_im)
|
assert_image_equal(original_im, reference_im)
|
||||||
|
|
||||||
png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png"))
|
roundtrip(original_im)
|
||||||
|
|
||||||
for png_path in png_paths:
|
|
||||||
with Image.open(png_path) as reference_im:
|
|
||||||
assert reference_im.mode == mode
|
|
||||||
|
|
||||||
path_no_ext = os.path.splitext(png_path)[0]
|
|
||||||
for origin, rle in product(_ORIGINS, (True, False)):
|
|
||||||
tga_path = "{}_{}_{}.tga".format(
|
|
||||||
path_no_ext, origin, "rle" if rle else "raw"
|
|
||||||
)
|
|
||||||
|
|
||||||
with Image.open(tga_path) as original_im:
|
|
||||||
assert original_im.format == "TGA"
|
|
||||||
assert original_im.get_format_mimetype() == "image/x-tga"
|
|
||||||
if rle:
|
|
||||||
assert original_im.info["compression"] == "tga_rle"
|
|
||||||
assert (
|
|
||||||
original_im.info["orientation"]
|
|
||||||
== _ORIGIN_TO_ORIENTATION[origin]
|
|
||||||
)
|
|
||||||
if mode == "P":
|
|
||||||
assert original_im.getpalette() == reference_im.getpalette()
|
|
||||||
|
|
||||||
assert_image_equal(original_im, reference_im)
|
|
||||||
|
|
||||||
roundtrip(original_im)
|
|
||||||
|
|
||||||
|
|
||||||
def test_palette_depth_16(tmp_path):
|
def test_palette_depth_16(tmp_path):
|
||||||
|
@ -101,6 +98,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:
|
||||||
|
@ -488,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:
|
||||||
|
@ -706,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")
|
||||||
|
|
||||||
|
|
|
@ -66,10 +66,10 @@ def test_load_set_dpi():
|
||||||
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
|
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
|
||||||
|
|
||||||
|
|
||||||
def test_save(tmp_path):
|
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
|
||||||
|
def test_save(ext, tmp_path):
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
||||||
for ext in [".wmf", ".emf"]:
|
tmpfile = str(tmp_path / ("temp" + ext))
|
||||||
tmpfile = str(tmp_path / ("temp" + ext))
|
with pytest.raises(OSError):
|
||||||
with pytest.raises(OSError):
|
im.save(tmpfile)
|
||||||
im.save(tmpfile)
|
|
||||||
|
|
|
@ -49,6 +49,14 @@ def test_sanity(request, tmp_path):
|
||||||
save_font(request, tmp_path)
|
save_font(request, tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_less_than_256_characters():
|
||||||
|
with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file:
|
||||||
|
font = PcfFontFile.PcfFontFile(test_file)
|
||||||
|
assert isinstance(font, FontFile.FontFile)
|
||||||
|
# check the number of characters in the font
|
||||||
|
assert len([_f for _f in font.glyph if _f]) == 127
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
|
@ -68,12 +76,19 @@ def test_textsize(request, tmp_path):
|
||||||
tempname = save_font(request, tmp_path)
|
tempname = save_font(request, tmp_path)
|
||||||
font = ImageFont.load(tempname)
|
font = ImageFont.load(tempname)
|
||||||
for i in range(255):
|
for i in range(255):
|
||||||
(dx, dy) = font.getsize(chr(i))
|
(ox, oy, dx, dy) = font.getbbox(chr(i))
|
||||||
|
assert ox == 0
|
||||||
|
assert oy == 0
|
||||||
assert dy == 20
|
assert dy == 20
|
||||||
assert dx in (0, 10)
|
assert dx in (0, 10)
|
||||||
|
assert font.getlength(chr(i)) == dx
|
||||||
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
assert font.getsize(chr(i)) == (dx, dy)
|
||||||
|
assert len(log) == 1
|
||||||
for i in range(len(message)):
|
for i in range(len(message)):
|
||||||
msg = message[: i + 1]
|
msg = message[: i + 1]
|
||||||
assert font.getsize(msg) == (len(msg) * 10, 20)
|
assert font.getlength(msg) == len(msg) * 10
|
||||||
|
assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20)
|
||||||
|
|
||||||
|
|
||||||
def _test_high_characters(request, tmp_path, message):
|
def _test_high_characters(request, tmp_path, message):
|
||||||
|
|
|
@ -101,13 +101,17 @@ def _test_textsize(request, tmp_path, encoding):
|
||||||
tempname = save_font(request, tmp_path, encoding)
|
tempname = save_font(request, tmp_path, encoding)
|
||||||
font = ImageFont.load(tempname)
|
font = ImageFont.load(tempname)
|
||||||
for i in range(255):
|
for i in range(255):
|
||||||
(dx, dy) = font.getsize(bytearray([i]))
|
(ox, oy, dx, dy) = font.getbbox(bytearray([i]))
|
||||||
|
assert ox == 0
|
||||||
|
assert oy == 0
|
||||||
assert dy == 20
|
assert dy == 20
|
||||||
assert dx in (0, 10)
|
assert dx in (0, 10)
|
||||||
|
assert font.getlength(bytearray([i])) == dx
|
||||||
message = charsets[encoding]["message"].encode(encoding)
|
message = charsets[encoding]["message"].encode(encoding)
|
||||||
for i in range(len(message)):
|
for i in range(len(message)):
|
||||||
msg = message[: i + 1]
|
msg = message[: i + 1]
|
||||||
assert font.getsize(msg) == (len(msg) * 10, 20)
|
assert font.getlength(msg) == len(msg) * 10
|
||||||
|
assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20)
|
||||||
|
|
||||||
|
|
||||||
def test_textsize_iso8859_1(request, tmp_path):
|
def test_textsize_iso8859_1(request, tmp_path):
|
||||||
|
|
|
@ -22,8 +22,9 @@ from .helper import (
|
||||||
|
|
||||||
|
|
||||||
class TestImage:
|
class TestImage:
|
||||||
def test_image_modes_success(self):
|
@pytest.mark.parametrize(
|
||||||
for mode in [
|
"mode",
|
||||||
|
(
|
||||||
"1",
|
"1",
|
||||||
"P",
|
"P",
|
||||||
"PA",
|
"PA",
|
||||||
|
@ -44,22 +45,18 @@ class TestImage:
|
||||||
"YCbCr",
|
"YCbCr",
|
||||||
"LAB",
|
"LAB",
|
||||||
"HSV",
|
"HSV",
|
||||||
]:
|
),
|
||||||
Image.new(mode, (1, 1))
|
)
|
||||||
|
def test_image_modes_success(self, mode):
|
||||||
|
Image.new(mode, (1, 1))
|
||||||
|
|
||||||
def test_image_modes_fail(self):
|
@pytest.mark.parametrize(
|
||||||
for mode in [
|
"mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32")
|
||||||
"",
|
)
|
||||||
"bad",
|
def test_image_modes_fail(self, mode):
|
||||||
"very very long",
|
with pytest.raises(ValueError) as e:
|
||||||
"BGR;15",
|
Image.new(mode, (1, 1))
|
||||||
"BGR;16",
|
assert str(e.value) == "unrecognized image mode"
|
||||||
"BGR;24",
|
|
||||||
"BGR;32",
|
|
||||||
]:
|
|
||||||
with pytest.raises(ValueError) as e:
|
|
||||||
Image.new(mode, (1, 1))
|
|
||||||
assert str(e.value) == "unrecognized image mode"
|
|
||||||
|
|
||||||
def test_exception_inheritance(self):
|
def test_exception_inheritance(self):
|
||||||
assert issubclass(UnidentifiedImageError, OSError)
|
assert issubclass(UnidentifiedImageError, OSError)
|
||||||
|
@ -539,23 +536,22 @@ class TestImage:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
Image.linear_gradient(wrong_mode)
|
Image.linear_gradient(wrong_mode)
|
||||||
|
|
||||||
def test_linear_gradient(self):
|
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
||||||
|
def test_linear_gradient(self, mode):
|
||||||
# Arrange
|
# Arrange
|
||||||
target_file = "Tests/images/linear_gradient.png"
|
target_file = "Tests/images/linear_gradient.png"
|
||||||
for mode in ["L", "P", "I", "F"]:
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
im = Image.linear_gradient(mode)
|
im = Image.linear_gradient(mode)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert im.size == (256, 256)
|
assert im.size == (256, 256)
|
||||||
assert im.mode == mode
|
assert im.mode == mode
|
||||||
assert im.getpixel((0, 0)) == 0
|
assert im.getpixel((0, 0)) == 0
|
||||||
assert im.getpixel((255, 255)) == 255
|
assert im.getpixel((255, 255)) == 255
|
||||||
with Image.open(target_file) as target:
|
with Image.open(target_file) as target:
|
||||||
target = target.convert(mode)
|
target = target.convert(mode)
|
||||||
assert_image_equal(im, target)
|
assert_image_equal(im, target)
|
||||||
|
|
||||||
def test_radial_gradient_wrong_mode(self):
|
def test_radial_gradient_wrong_mode(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
|
@ -565,23 +561,22 @@ class TestImage:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
Image.radial_gradient(wrong_mode)
|
Image.radial_gradient(wrong_mode)
|
||||||
|
|
||||||
def test_radial_gradient(self):
|
@pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
|
||||||
|
def test_radial_gradient(self, mode):
|
||||||
# Arrange
|
# Arrange
|
||||||
target_file = "Tests/images/radial_gradient.png"
|
target_file = "Tests/images/radial_gradient.png"
|
||||||
for mode in ["L", "P", "I", "F"]:
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
im = Image.radial_gradient(mode)
|
im = Image.radial_gradient(mode)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert im.size == (256, 256)
|
assert im.size == (256, 256)
|
||||||
assert im.mode == mode
|
assert im.mode == mode
|
||||||
assert im.getpixel((0, 0)) == 255
|
assert im.getpixel((0, 0)) == 255
|
||||||
assert im.getpixel((128, 128)) == 0
|
assert im.getpixel((128, 128)) == 0
|
||||||
with Image.open(target_file) as target:
|
with Image.open(target_file) as target:
|
||||||
target = target.convert(mode)
|
target = target.convert(mode)
|
||||||
assert_image_equal(im, target)
|
assert_image_equal(im, target)
|
||||||
|
|
||||||
def test_register_extensions(self):
|
def test_register_extensions(self):
|
||||||
test_format = "a"
|
test_format = "a"
|
||||||
|
@ -604,11 +599,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")
|
||||||
|
@ -826,6 +844,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
|
||||||
|
|
|
@ -184,8 +184,9 @@ class TestImageGetPixel(AccessTest):
|
||||||
with pytest.raises(error):
|
with pytest.raises(error):
|
||||||
im.getpixel((-1, -1))
|
im.getpixel((-1, -1))
|
||||||
|
|
||||||
def test_basic(self):
|
@pytest.mark.parametrize(
|
||||||
for mode in (
|
"mode",
|
||||||
|
(
|
||||||
"1",
|
"1",
|
||||||
"L",
|
"L",
|
||||||
"LA",
|
"LA",
|
||||||
|
@ -200,23 +201,25 @@ class TestImageGetPixel(AccessTest):
|
||||||
"RGBX",
|
"RGBX",
|
||||||
"CMYK",
|
"CMYK",
|
||||||
"YCbCr",
|
"YCbCr",
|
||||||
):
|
),
|
||||||
self.check(mode)
|
)
|
||||||
|
def test_basic(self, mode):
|
||||||
|
self.check(mode)
|
||||||
|
|
||||||
def test_signedness(self):
|
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||||
|
def test_signedness(self, mode):
|
||||||
# see https://github.com/python-pillow/Pillow/issues/452
|
# see https://github.com/python-pillow/Pillow/issues/452
|
||||||
# pixelaccess is using signed int* instead of uint*
|
# pixelaccess is using signed int* instead of uint*
|
||||||
for mode in ("I;16", "I;16B"):
|
self.check(mode, 2**15 - 1)
|
||||||
self.check(mode, 2**15 - 1)
|
self.check(mode, 2**15)
|
||||||
self.check(mode, 2**15)
|
self.check(mode, 2**15 + 1)
|
||||||
self.check(mode, 2**15 + 1)
|
self.check(mode, 2**16 - 1)
|
||||||
self.check(mode, 2**16 - 1)
|
|
||||||
|
|
||||||
def test_p_putpixel_rgb_rgba(self):
|
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
||||||
for color in [(255, 0, 0), (255, 0, 0, 255)]:
|
def test_p_putpixel_rgb_rgba(self, color):
|
||||||
im = Image.new("P", (1, 1), 0)
|
im = Image.new("P", (1, 1), 0)
|
||||||
im.putpixel((0, 0), color)
|
im.putpixel((0, 0), color)
|
||||||
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
|
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
@ -34,9 +35,10 @@ def test_toarray():
|
||||||
test_with_dtype(numpy.float64)
|
test_with_dtype(numpy.float64)
|
||||||
test_with_dtype(numpy.uint8)
|
test_with_dtype(numpy.uint8)
|
||||||
|
|
||||||
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
|
if parse_version(numpy.__version__) >= parse_version("1.23"):
|
||||||
with pytest.raises(OSError):
|
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
|
||||||
numpy.array(im_truncated)
|
with pytest.raises(OSError):
|
||||||
|
numpy.array(im_truncated)
|
||||||
|
|
||||||
|
|
||||||
def test_fromarray():
|
def test_fromarray():
|
||||||
|
@ -80,3 +82,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
|
||||||
|
|
|
@ -222,6 +222,20 @@ def test_p_la():
|
||||||
assert_image_similar(alpha, comparable, 5)
|
assert_image_similar(alpha, comparable, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_p2pa_alpha():
|
||||||
|
with Image.open("Tests/images/tiny.png") as im:
|
||||||
|
assert im.mode == "P"
|
||||||
|
|
||||||
|
im_pa = im.convert("PA")
|
||||||
|
assert im_pa.mode == "PA"
|
||||||
|
|
||||||
|
im_a = im_pa.getchannel("A")
|
||||||
|
for x in range(4):
|
||||||
|
alpha = 255 if x > 1 else 0
|
||||||
|
for y in range(4):
|
||||||
|
assert im_a.getpixel((x, y)) == alpha
|
||||||
|
|
||||||
|
|
||||||
def test_matrix_illegal_conversion():
|
def test_matrix_illegal_conversion():
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper("CMYK")
|
im = hopper("CMYK")
|
||||||
|
@ -254,36 +268,33 @@ def test_matrix_wrong_mode():
|
||||||
im.convert(mode="L", matrix=matrix)
|
im.convert(mode="L", matrix=matrix)
|
||||||
|
|
||||||
|
|
||||||
def test_matrix_xyz():
|
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
||||||
def matrix_convert(mode):
|
def test_matrix_xyz(mode):
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
im.info["transparency"] = (255, 0, 0)
|
im.info["transparency"] = (255, 0, 0)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
matrix = (
|
matrix = (
|
||||||
0.412453, 0.357580, 0.180423, 0,
|
0.412453, 0.357580, 0.180423, 0,
|
||||||
0.212671, 0.715160, 0.072169, 0,
|
0.212671, 0.715160, 0.072169, 0,
|
||||||
0.019334, 0.119193, 0.950227, 0)
|
0.019334, 0.119193, 0.950227, 0)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
# Convert an RGB image to the CIE XYZ colour space
|
# Convert an RGB image to the CIE XYZ colour space
|
||||||
converted_im = im.convert(mode=mode, matrix=matrix)
|
converted_im = im.convert(mode=mode, matrix=matrix)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert converted_im.mode == mode
|
assert converted_im.mode == mode
|
||||||
assert converted_im.size == im.size
|
assert converted_im.size == im.size
|
||||||
with Image.open("Tests/images/hopper-XYZ.png") as target:
|
with Image.open("Tests/images/hopper-XYZ.png") as target:
|
||||||
if converted_im.mode == "RGB":
|
if converted_im.mode == "RGB":
|
||||||
assert_image_similar(converted_im, target, 3)
|
assert_image_similar(converted_im, target, 3)
|
||||||
assert converted_im.info["transparency"] == (105, 54, 4)
|
assert converted_im.info["transparency"] == (105, 54, 4)
|
||||||
else:
|
else:
|
||||||
assert_image_similar(converted_im, target.getchannel(0), 1)
|
assert_image_similar(converted_im, target.getchannel(0), 1)
|
||||||
assert converted_im.info["transparency"] == 105
|
assert converted_im.info["transparency"] == 105
|
||||||
|
|
||||||
matrix_convert("RGB")
|
|
||||||
matrix_convert("L")
|
|
||||||
|
|
||||||
|
|
||||||
def test_matrix_identity():
|
def test_matrix_identity():
|
||||||
|
|
|
@ -1,37 +1,40 @@
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper
|
||||||
|
|
||||||
|
|
||||||
def test_copy():
|
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
|
||||||
|
def test_copy(mode):
|
||||||
cropped_coordinates = (10, 10, 20, 20)
|
cropped_coordinates = (10, 10, 20, 20)
|
||||||
cropped_size = (10, 10)
|
cropped_size = (10, 10)
|
||||||
for mode in "1", "P", "L", "RGB", "I", "F":
|
|
||||||
# Internal copy method
|
|
||||||
im = hopper(mode)
|
|
||||||
out = im.copy()
|
|
||||||
assert out.mode == im.mode
|
|
||||||
assert out.size == im.size
|
|
||||||
|
|
||||||
# Python's copy method
|
# Internal copy method
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
out = copy.copy(im)
|
out = im.copy()
|
||||||
assert out.mode == im.mode
|
assert out.mode == im.mode
|
||||||
assert out.size == im.size
|
assert out.size == im.size
|
||||||
|
|
||||||
# Internal copy method on a cropped image
|
# Python's copy method
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
out = im.crop(cropped_coordinates).copy()
|
out = copy.copy(im)
|
||||||
assert out.mode == im.mode
|
assert out.mode == im.mode
|
||||||
assert out.size == cropped_size
|
assert out.size == im.size
|
||||||
|
|
||||||
# Python's copy method on a cropped image
|
# Internal copy method on a cropped image
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
out = copy.copy(im.crop(cropped_coordinates))
|
out = im.crop(cropped_coordinates).copy()
|
||||||
assert out.mode == im.mode
|
assert out.mode == im.mode
|
||||||
assert out.size == cropped_size
|
assert out.size == cropped_size
|
||||||
|
|
||||||
|
# Python's copy method on a cropped image
|
||||||
|
im = hopper(mode)
|
||||||
|
out = copy.copy(im.crop(cropped_coordinates))
|
||||||
|
assert out.mode == im.mode
|
||||||
|
assert out.size == cropped_size
|
||||||
|
|
||||||
|
|
||||||
def test_copy_zero():
|
def test_copy_zero():
|
||||||
|
|
|
@ -5,17 +5,14 @@ from PIL import Image
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
|
||||||
|
|
||||||
def test_crop():
|
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
|
||||||
def crop(mode):
|
def test_crop(mode):
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
assert_image_equal(im.crop(), im)
|
assert_image_equal(im.crop(), im)
|
||||||
|
|
||||||
cropped = im.crop((50, 50, 100, 100))
|
cropped = im.crop((50, 50, 100, 100))
|
||||||
assert cropped.mode == mode
|
assert cropped.mode == mode
|
||||||
assert cropped.size == (50, 50)
|
assert cropped.size == (50, 50)
|
||||||
|
|
||||||
for mode in "1", "P", "L", "RGB", "I", "F":
|
|
||||||
crop(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_wide_crop():
|
def test_wide_crop():
|
||||||
|
|
|
@ -9,7 +9,7 @@ def test_entropy():
|
||||||
assert round(abs(entropy("L") - 7.063008716585465), 7) == 0
|
assert round(abs(entropy("L") - 7.063008716585465), 7) == 0
|
||||||
assert round(abs(entropy("I") - 7.063008716585465), 7) == 0
|
assert round(abs(entropy("I") - 7.063008716585465), 7) == 0
|
||||||
assert round(abs(entropy("F") - 7.063008716585465), 7) == 0
|
assert round(abs(entropy("F") - 7.063008716585465), 7) == 0
|
||||||
assert round(abs(entropy("P") - 5.0530452472519745), 7) == 0
|
assert round(abs(entropy("P") - 5.082506854662517), 7) == 0
|
||||||
assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0
|
assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0
|
||||||
assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0
|
assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0
|
||||||
assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0
|
assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0
|
||||||
|
|
|
@ -16,7 +16,7 @@ def test_getcolors():
|
||||||
assert getcolors("L") == 255
|
assert getcolors("L") == 255
|
||||||
assert getcolors("I") == 255
|
assert getcolors("I") == 255
|
||||||
assert getcolors("F") == 255
|
assert getcolors("F") == 255
|
||||||
assert getcolors("P") == 90 # fixed palette
|
assert getcolors("P") == 96 # fixed palette
|
||||||
assert getcolors("RGB") is None
|
assert getcolors("RGB") is None
|
||||||
assert getcolors("RGBA") is None
|
assert getcolors("RGBA") is None
|
||||||
assert getcolors("CMYK") is None
|
assert getcolors("CMYK") is None
|
||||||
|
|
|
@ -10,7 +10,7 @@ def test_histogram():
|
||||||
assert histogram("L") == (256, 0, 662)
|
assert histogram("L") == (256, 0, 662)
|
||||||
assert histogram("I") == (256, 0, 662)
|
assert histogram("I") == (256, 0, 662)
|
||||||
assert histogram("F") == (256, 0, 662)
|
assert histogram("F") == (256, 0, 662)
|
||||||
assert histogram("P") == (256, 0, 1871)
|
assert histogram("P") == (256, 0, 1551)
|
||||||
assert histogram("RGB") == (768, 4, 675)
|
assert histogram("RGB") == (768, 4, 675)
|
||||||
assert histogram("RGBA") == (1024, 0, 16384)
|
assert histogram("RGBA") == (1024, 0, 16384)
|
||||||
assert histogram("CMYK") == (1024, 0, 16384)
|
assert histogram("CMYK") == (1024, 0, 16384)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import CachedProperty, assert_image_equal
|
from .helper import CachedProperty, assert_image_equal
|
||||||
|
@ -101,226 +103,226 @@ class TestImagingPaste:
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_image_solid(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_image_solid(self, mode):
|
||||||
im = Image.new(mode, (200, 200), "red")
|
im = Image.new(mode, (200, 200), "red")
|
||||||
im2 = getattr(self, "gradient_" + mode)
|
im2 = getattr(self, "gradient_" + mode)
|
||||||
|
|
||||||
im.paste(im2, (12, 23))
|
im.paste(im2, (12, 23))
|
||||||
|
|
||||||
im = im.crop((12, 23, im2.width + 12, im2.height + 23))
|
im = im.crop((12, 23, im2.width + 12, im2.height + 23))
|
||||||
assert_image_equal(im, im2)
|
assert_image_equal(im, im2)
|
||||||
|
|
||||||
def test_image_mask_1(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_image_mask_1(self, mode):
|
||||||
im = Image.new(mode, (200, 200), "white")
|
im = Image.new(mode, (200, 200), "white")
|
||||||
im2 = getattr(self, "gradient_" + mode)
|
im2 = getattr(self, "gradient_" + mode)
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
im2,
|
im2,
|
||||||
self.mask_1,
|
self.mask_1,
|
||||||
[
|
[
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
(127, 254, 127, 0),
|
(127, 254, 127, 0),
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
(191, 190, 63, 64),
|
(191, 190, 63, 64),
|
||||||
(127, 0, 127, 254),
|
(127, 0, 127, 254),
|
||||||
(191, 64, 63, 190),
|
(191, 64, 63, 190),
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_image_mask_L(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_image_mask_L(self, mode):
|
||||||
im = Image.new(mode, (200, 200), "white")
|
im = Image.new(mode, (200, 200), "white")
|
||||||
im2 = getattr(self, "gradient_" + mode)
|
im2 = getattr(self, "gradient_" + mode)
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
im2,
|
im2,
|
||||||
self.mask_L,
|
self.mask_L,
|
||||||
[
|
[
|
||||||
(128, 191, 255, 191),
|
(128, 191, 255, 191),
|
||||||
(208, 239, 239, 208),
|
(208, 239, 239, 208),
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
(112, 111, 206, 207),
|
(112, 111, 206, 207),
|
||||||
(192, 191, 191, 191),
|
(192, 191, 191, 191),
|
||||||
(239, 239, 207, 207),
|
(239, 239, 207, 207),
|
||||||
(128, 1, 128, 254),
|
(128, 1, 128, 254),
|
||||||
(207, 113, 112, 207),
|
(207, 113, 112, 207),
|
||||||
(255, 191, 128, 191),
|
(255, 191, 128, 191),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_image_mask_LA(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_image_mask_LA(self, mode):
|
||||||
im = Image.new(mode, (200, 200), "white")
|
im = Image.new(mode, (200, 200), "white")
|
||||||
im2 = getattr(self, "gradient_" + mode)
|
im2 = getattr(self, "gradient_" + mode)
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
im2,
|
im2,
|
||||||
self.gradient_LA,
|
self.gradient_LA,
|
||||||
[
|
[
|
||||||
(128, 191, 255, 191),
|
(128, 191, 255, 191),
|
||||||
(112, 207, 206, 111),
|
(112, 207, 206, 111),
|
||||||
(128, 254, 128, 1),
|
(128, 254, 128, 1),
|
||||||
(208, 208, 239, 239),
|
(208, 208, 239, 239),
|
||||||
(192, 191, 191, 191),
|
(192, 191, 191, 191),
|
||||||
(207, 207, 112, 113),
|
(207, 207, 112, 113),
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
(239, 207, 207, 239),
|
(239, 207, 207, 239),
|
||||||
(255, 191, 128, 191),
|
(255, 191, 128, 191),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_image_mask_RGBA(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_image_mask_RGBA(self, mode):
|
||||||
im = Image.new(mode, (200, 200), "white")
|
im = Image.new(mode, (200, 200), "white")
|
||||||
im2 = getattr(self, "gradient_" + mode)
|
im2 = getattr(self, "gradient_" + mode)
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
im2,
|
im2,
|
||||||
self.gradient_RGBA,
|
self.gradient_RGBA,
|
||||||
[
|
[
|
||||||
(128, 191, 255, 191),
|
(128, 191, 255, 191),
|
||||||
(208, 239, 239, 208),
|
(208, 239, 239, 208),
|
||||||
(255, 255, 255, 255),
|
(255, 255, 255, 255),
|
||||||
(112, 111, 206, 207),
|
(112, 111, 206, 207),
|
||||||
(192, 191, 191, 191),
|
(192, 191, 191, 191),
|
||||||
(239, 239, 207, 207),
|
(239, 239, 207, 207),
|
||||||
(128, 1, 128, 254),
|
(128, 1, 128, 254),
|
||||||
(207, 113, 112, 207),
|
(207, 113, 112, 207),
|
||||||
(255, 191, 128, 191),
|
(255, 191, 128, 191),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_image_mask_RGBa(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_image_mask_RGBa(self, mode):
|
||||||
im = Image.new(mode, (200, 200), "white")
|
im = Image.new(mode, (200, 200), "white")
|
||||||
im2 = getattr(self, "gradient_" + mode)
|
im2 = getattr(self, "gradient_" + mode)
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
im2,
|
im2,
|
||||||
self.gradient_RGBa,
|
self.gradient_RGBa,
|
||||||
[
|
[
|
||||||
(128, 255, 126, 255),
|
(128, 255, 126, 255),
|
||||||
(0, 127, 126, 255),
|
(0, 127, 126, 255),
|
||||||
(126, 253, 126, 255),
|
(126, 253, 126, 255),
|
||||||
(128, 127, 254, 255),
|
(128, 127, 254, 255),
|
||||||
(0, 255, 254, 255),
|
(0, 255, 254, 255),
|
||||||
(126, 125, 254, 255),
|
(126, 125, 254, 255),
|
||||||
(128, 1, 128, 255),
|
(128, 1, 128, 255),
|
||||||
(0, 129, 128, 255),
|
(0, 129, 128, 255),
|
||||||
(126, 255, 128, 255),
|
(126, 255, 128, 255),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_color_solid(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_color_solid(self, mode):
|
||||||
im = Image.new(mode, (200, 200), "black")
|
im = Image.new(mode, (200, 200), "black")
|
||||||
|
|
||||||
rect = (12, 23, 128 + 12, 128 + 23)
|
rect = (12, 23, 128 + 12, 128 + 23)
|
||||||
im.paste("white", rect)
|
im.paste("white", rect)
|
||||||
|
|
||||||
hist = im.crop(rect).histogram()
|
hist = im.crop(rect).histogram()
|
||||||
while hist:
|
while hist:
|
||||||
head, hist = hist[:256], hist[256:]
|
head, hist = hist[:256], hist[256:]
|
||||||
assert head[255] == 128 * 128
|
assert head[255] == 128 * 128
|
||||||
assert sum(head[:255]) == 0
|
assert sum(head[:255]) == 0
|
||||||
|
|
||||||
def test_color_mask_1(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_color_mask_1(self, mode):
|
||||||
im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
|
im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
|
||||||
color = (10, 20, 30, 40)[: len(mode)]
|
color = (10, 20, 30, 40)[: len(mode)]
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
color,
|
color,
|
||||||
self.mask_1,
|
self.mask_1,
|
||||||
[
|
[
|
||||||
(50, 60, 70, 80),
|
(50, 60, 70, 80),
|
||||||
(50, 60, 70, 80),
|
(50, 60, 70, 80),
|
||||||
(10, 20, 30, 40),
|
(10, 20, 30, 40),
|
||||||
(50, 60, 70, 80),
|
(50, 60, 70, 80),
|
||||||
(50, 60, 70, 80),
|
(50, 60, 70, 80),
|
||||||
(10, 20, 30, 40),
|
(10, 20, 30, 40),
|
||||||
(10, 20, 30, 40),
|
(10, 20, 30, 40),
|
||||||
(10, 20, 30, 40),
|
(10, 20, 30, 40),
|
||||||
(50, 60, 70, 80),
|
(50, 60, 70, 80),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_color_mask_L(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_color_mask_L(self, mode):
|
||||||
im = getattr(self, "gradient_" + mode).copy()
|
im = getattr(self, "gradient_" + mode).copy()
|
||||||
color = "white"
|
color = "white"
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
color,
|
color,
|
||||||
self.mask_L,
|
self.mask_L,
|
||||||
[
|
[
|
||||||
(127, 191, 254, 191),
|
(127, 191, 254, 191),
|
||||||
(111, 207, 206, 110),
|
(111, 207, 206, 110),
|
||||||
(127, 254, 127, 0),
|
(127, 254, 127, 0),
|
||||||
(207, 207, 239, 239),
|
(207, 207, 239, 239),
|
||||||
(191, 191, 190, 191),
|
(191, 191, 190, 191),
|
||||||
(207, 206, 111, 112),
|
(207, 206, 111, 112),
|
||||||
(254, 254, 254, 255),
|
(254, 254, 254, 255),
|
||||||
(239, 206, 206, 238),
|
(239, 206, 206, 238),
|
||||||
(254, 191, 127, 191),
|
(254, 191, 127, 191),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_color_mask_RGBA(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_color_mask_RGBA(self, mode):
|
||||||
im = getattr(self, "gradient_" + mode).copy()
|
im = getattr(self, "gradient_" + mode).copy()
|
||||||
color = "white"
|
color = "white"
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
color,
|
color,
|
||||||
self.gradient_RGBA,
|
self.gradient_RGBA,
|
||||||
[
|
[
|
||||||
(127, 191, 254, 191),
|
(127, 191, 254, 191),
|
||||||
(111, 207, 206, 110),
|
(111, 207, 206, 110),
|
||||||
(127, 254, 127, 0),
|
(127, 254, 127, 0),
|
||||||
(207, 207, 239, 239),
|
(207, 207, 239, 239),
|
||||||
(191, 191, 190, 191),
|
(191, 191, 190, 191),
|
||||||
(207, 206, 111, 112),
|
(207, 206, 111, 112),
|
||||||
(254, 254, 254, 255),
|
(254, 254, 254, 255),
|
||||||
(239, 206, 206, 238),
|
(239, 206, 206, 238),
|
||||||
(254, 191, 127, 191),
|
(254, 191, 127, 191),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_color_mask_RGBa(self):
|
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||||
for mode in ("RGBA", "RGB", "L"):
|
def test_color_mask_RGBa(self, mode):
|
||||||
im = getattr(self, "gradient_" + mode).copy()
|
im = getattr(self, "gradient_" + mode).copy()
|
||||||
color = "white"
|
color = "white"
|
||||||
|
|
||||||
self.assert_9points_paste(
|
self.assert_9points_paste(
|
||||||
im,
|
im,
|
||||||
color,
|
color,
|
||||||
self.gradient_RGBa,
|
self.gradient_RGBa,
|
||||||
[
|
[
|
||||||
(255, 63, 126, 63),
|
(255, 63, 126, 63),
|
||||||
(47, 143, 142, 46),
|
(47, 143, 142, 46),
|
||||||
(126, 253, 126, 255),
|
(126, 253, 126, 255),
|
||||||
(15, 15, 47, 47),
|
(15, 15, 47, 47),
|
||||||
(63, 63, 62, 63),
|
(63, 63, 62, 63),
|
||||||
(142, 141, 46, 47),
|
(142, 141, 46, 47),
|
||||||
(255, 255, 255, 0),
|
(255, 255, 255, 0),
|
||||||
(48, 15, 15, 47),
|
(48, 15, 15, 47),
|
||||||
(126, 63, 255, 63),
|
(126, 63, 255, 63),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_different_sizes(self):
|
def test_different_sizes(self):
|
||||||
im = Image.new("RGB", (100, 100))
|
im = Image.new("RGB", (100, 100))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,11 +19,24 @@ def test_sanity():
|
||||||
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():
|
||||||
|
@ -47,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
|
||||||
|
|
|
@ -65,6 +65,22 @@ def test_quantize_no_dither():
|
||||||
assert converted.palette.palette == palette.palette.palette
|
assert converted.palette.palette == palette.palette.palette
|
||||||
|
|
||||||
|
|
||||||
|
def test_quantize_no_dither2():
|
||||||
|
im = Image.new("RGB", (9, 1))
|
||||||
|
im.putdata(list((p,) * 3 for p in range(0, 36, 4)))
|
||||||
|
|
||||||
|
palette = Image.new("P", (1, 1))
|
||||||
|
data = (0, 0, 0, 32, 32, 32)
|
||||||
|
palette.putpalette(data)
|
||||||
|
quantized = im.quantize(dither=Image.Dither.NONE, palette=palette)
|
||||||
|
|
||||||
|
assert tuple(quantized.palette.palette) == data
|
||||||
|
|
||||||
|
px = quantized.load()
|
||||||
|
for x in range(9):
|
||||||
|
assert px[x, 0] == (0 if x < 5 else 1)
|
||||||
|
|
||||||
|
|
||||||
def test_quantize_dither_diff():
|
def test_quantize_dither_diff():
|
||||||
image = hopper()
|
image = hopper()
|
||||||
with Image.open("Tests/images/caption_6_33_22.png") as palette:
|
with Image.open("Tests/images/caption_6_33_22.png") as palette:
|
||||||
|
|
|
@ -100,40 +100,41 @@ class TestImagingCoreResampleAccuracy:
|
||||||
for y in range(image.size[1])
|
for y in range(image.size[1])
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_reduce_box(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_reduce_box(self, mode):
|
||||||
case = self.make_case(mode, (8, 8), 0xE1)
|
case = self.make_case(mode, (8, 8), 0xE1)
|
||||||
case = case.resize((4, 4), Image.Resampling.BOX)
|
case = case.resize((4, 4), Image.Resampling.BOX)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 e1"
|
data = ("e1 e1"
|
||||||
"e1 e1")
|
"e1 e1")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||||
|
|
||||||
def test_reduce_bilinear(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_reduce_bilinear(self, mode):
|
||||||
case = self.make_case(mode, (8, 8), 0xE1)
|
case = self.make_case(mode, (8, 8), 0xE1)
|
||||||
case = case.resize((4, 4), Image.Resampling.BILINEAR)
|
case = case.resize((4, 4), Image.Resampling.BILINEAR)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 c9"
|
data = ("e1 c9"
|
||||||
"c9 b7")
|
"c9 b7")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||||
|
|
||||||
def test_reduce_hamming(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_reduce_hamming(self, mode):
|
||||||
case = self.make_case(mode, (8, 8), 0xE1)
|
case = self.make_case(mode, (8, 8), 0xE1)
|
||||||
case = case.resize((4, 4), Image.Resampling.HAMMING)
|
case = case.resize((4, 4), Image.Resampling.HAMMING)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 da"
|
data = ("e1 da"
|
||||||
"da d3")
|
"da d3")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||||
|
|
||||||
def test_reduce_bicubic(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
|
def test_reduce_bicubic(self, mode):
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
for mode in ["RGBX", "RGB", "La", "L"]:
|
||||||
case = self.make_case(mode, (12, 12), 0xE1)
|
case = self.make_case(mode, (12, 12), 0xE1)
|
||||||
case = case.resize((6, 6), Image.Resampling.BICUBIC)
|
case = case.resize((6, 6), Image.Resampling.BICUBIC)
|
||||||
|
@ -145,79 +146,79 @@ class TestImagingCoreResampleAccuracy:
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (6, 6)))
|
self.check_case(channel, self.make_sample(data, (6, 6)))
|
||||||
|
|
||||||
def test_reduce_lanczos(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_reduce_lanczos(self, mode):
|
||||||
case = self.make_case(mode, (16, 16), 0xE1)
|
case = self.make_case(mode, (16, 16), 0xE1)
|
||||||
case = case.resize((8, 8), Image.Resampling.LANCZOS)
|
case = case.resize((8, 8), Image.Resampling.LANCZOS)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 e0 e4 d7"
|
data = ("e1 e0 e4 d7"
|
||||||
"e0 df e3 d6"
|
"e0 df e3 d6"
|
||||||
"e4 e3 e7 da"
|
"e4 e3 e7 da"
|
||||||
"d7 d6 d9 ce")
|
"d7 d6 d9 ce")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (8, 8)))
|
self.check_case(channel, self.make_sample(data, (8, 8)))
|
||||||
|
|
||||||
def test_enlarge_box(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_enlarge_box(self, mode):
|
||||||
case = self.make_case(mode, (2, 2), 0xE1)
|
case = self.make_case(mode, (2, 2), 0xE1)
|
||||||
case = case.resize((4, 4), Image.Resampling.BOX)
|
case = case.resize((4, 4), Image.Resampling.BOX)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 e1"
|
data = ("e1 e1"
|
||||||
"e1 e1")
|
"e1 e1")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||||
|
|
||||||
def test_enlarge_bilinear(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_enlarge_bilinear(self, mode):
|
||||||
case = self.make_case(mode, (2, 2), 0xE1)
|
case = self.make_case(mode, (2, 2), 0xE1)
|
||||||
case = case.resize((4, 4), Image.Resampling.BILINEAR)
|
case = case.resize((4, 4), Image.Resampling.BILINEAR)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 b0"
|
data = ("e1 b0"
|
||||||
"b0 98")
|
"b0 98")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||||
|
|
||||||
def test_enlarge_hamming(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_enlarge_hamming(self, mode):
|
||||||
case = self.make_case(mode, (2, 2), 0xE1)
|
case = self.make_case(mode, (2, 2), 0xE1)
|
||||||
case = case.resize((4, 4), Image.Resampling.HAMMING)
|
case = case.resize((4, 4), Image.Resampling.HAMMING)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 d2"
|
data = ("e1 d2"
|
||||||
"d2 c5")
|
"d2 c5")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||||
|
|
||||||
def test_enlarge_bicubic(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_enlarge_bicubic(self, mode):
|
||||||
case = self.make_case(mode, (4, 4), 0xE1)
|
case = self.make_case(mode, (4, 4), 0xE1)
|
||||||
case = case.resize((8, 8), Image.Resampling.BICUBIC)
|
case = case.resize((8, 8), Image.Resampling.BICUBIC)
|
||||||
# fmt: off
|
# fmt: off
|
||||||
data = ("e1 e5 ee b9"
|
data = ("e1 e5 ee b9"
|
||||||
"e5 e9 f3 bc"
|
"e5 e9 f3 bc"
|
||||||
"ee f3 fd c1"
|
"ee f3 fd c1"
|
||||||
"b9 bc c1 a2")
|
"b9 bc c1 a2")
|
||||||
# fmt: on
|
# fmt: on
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (8, 8)))
|
self.check_case(channel, self.make_sample(data, (8, 8)))
|
||||||
|
|
||||||
def test_enlarge_lanczos(self):
|
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
|
||||||
for mode in ["RGBX", "RGB", "La", "L"]:
|
def test_enlarge_lanczos(self, mode):
|
||||||
case = self.make_case(mode, (6, 6), 0xE1)
|
case = self.make_case(mode, (6, 6), 0xE1)
|
||||||
case = case.resize((12, 12), Image.Resampling.LANCZOS)
|
case = case.resize((12, 12), Image.Resampling.LANCZOS)
|
||||||
data = (
|
data = (
|
||||||
"e1 e0 db ed f5 b8"
|
"e1 e0 db ed f5 b8"
|
||||||
"e0 df da ec f3 b7"
|
"e0 df da ec f3 b7"
|
||||||
"db db d6 e7 ee b5"
|
"db db d6 e7 ee b5"
|
||||||
"ed ec e6 fb ff bf"
|
"ed ec e6 fb ff bf"
|
||||||
"f5 f4 ee ff ff c4"
|
"f5 f4 ee ff ff c4"
|
||||||
"b8 b7 b4 bf c4 a0"
|
"b8 b7 b4 bf c4 a0"
|
||||||
)
|
)
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (12, 12)))
|
self.check_case(channel, self.make_sample(data, (12, 12)))
|
||||||
|
|
||||||
def test_box_filter_correct_range(self):
|
def test_box_filter_correct_range(self):
|
||||||
im = Image.new("RGB", (8, 8), "#1688ff").resize(
|
im = Image.new("RGB", (8, 8), "#1688ff").resize(
|
||||||
|
@ -419,40 +420,43 @@ class TestCoreResampleCoefficients:
|
||||||
|
|
||||||
|
|
||||||
class TestCoreResampleBox:
|
class TestCoreResampleBox:
|
||||||
def test_wrong_arguments(self):
|
@pytest.mark.parametrize(
|
||||||
im = hopper()
|
"resample",
|
||||||
for resample in (
|
(
|
||||||
Image.Resampling.NEAREST,
|
Image.Resampling.NEAREST,
|
||||||
Image.Resampling.BOX,
|
Image.Resampling.BOX,
|
||||||
Image.Resampling.BILINEAR,
|
Image.Resampling.BILINEAR,
|
||||||
Image.Resampling.HAMMING,
|
Image.Resampling.HAMMING,
|
||||||
Image.Resampling.BICUBIC,
|
Image.Resampling.BICUBIC,
|
||||||
Image.Resampling.LANCZOS,
|
Image.Resampling.LANCZOS,
|
||||||
):
|
),
|
||||||
im.resize((32, 32), resample, (0, 0, im.width, im.height))
|
)
|
||||||
im.resize((32, 32), resample, (20, 20, im.width, im.height))
|
def test_wrong_arguments(self, resample):
|
||||||
im.resize((32, 32), resample, (20, 20, 20, 100))
|
im = hopper()
|
||||||
im.resize((32, 32), resample, (20, 20, 100, 20))
|
im.resize((32, 32), resample, (0, 0, im.width, im.height))
|
||||||
|
im.resize((32, 32), resample, (20, 20, im.width, im.height))
|
||||||
|
im.resize((32, 32), resample, (20, 20, 20, 100))
|
||||||
|
im.resize((32, 32), resample, (20, 20, 100, 20))
|
||||||
|
|
||||||
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
with pytest.raises(TypeError, match="must be sequence of length 4"):
|
||||||
im.resize((32, 32), resample, (im.width, im.height))
|
im.resize((32, 32), resample, (im.width, im.height))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="can't be negative"):
|
with pytest.raises(ValueError, match="can't be negative"):
|
||||||
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
im.resize((32, 32), resample, (-20, 20, 100, 100))
|
||||||
with pytest.raises(ValueError, match="can't be negative"):
|
with pytest.raises(ValueError, match="can't be negative"):
|
||||||
im.resize((32, 32), resample, (20, -20, 100, 100))
|
im.resize((32, 32), resample, (20, -20, 100, 100))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="can't be empty"):
|
with pytest.raises(ValueError, match="can't be empty"):
|
||||||
im.resize((32, 32), resample, (20.1, 20, 20, 100))
|
im.resize((32, 32), resample, (20.1, 20, 20, 100))
|
||||||
with pytest.raises(ValueError, match="can't be empty"):
|
with pytest.raises(ValueError, match="can't be empty"):
|
||||||
im.resize((32, 32), resample, (20, 20.1, 100, 20))
|
im.resize((32, 32), resample, (20, 20.1, 100, 20))
|
||||||
with pytest.raises(ValueError, match="can't be empty"):
|
with pytest.raises(ValueError, match="can't be empty"):
|
||||||
im.resize((32, 32), resample, (20.1, 20.1, 20, 20))
|
im.resize((32, 32), resample, (20.1, 20.1, 20, 20))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="can't exceed"):
|
with pytest.raises(ValueError, match="can't exceed"):
|
||||||
im.resize((32, 32), resample, (0, 0, im.width + 1, im.height))
|
im.resize((32, 32), resample, (0, 0, im.width + 1, im.height))
|
||||||
with pytest.raises(ValueError, match="can't exceed"):
|
with pytest.raises(ValueError, match="can't exceed"):
|
||||||
im.resize((32, 32), resample, (0, 0, im.width, im.height + 1))
|
im.resize((32, 32), resample, (0, 0, im.width, im.height + 1))
|
||||||
|
|
||||||
def resize_tiled(self, im, dst_size, xtiles, ytiles):
|
def resize_tiled(self, im, dst_size, xtiles, ytiles):
|
||||||
def split_range(size, tiles):
|
def split_range(size, tiles):
|
||||||
|
@ -509,14 +513,16 @@ class TestCoreResampleBox:
|
||||||
with pytest.raises(AssertionError, match=r"difference 29\."):
|
with pytest.raises(AssertionError, match=r"difference 29\."):
|
||||||
assert_image_similar(reference, without_box, 5)
|
assert_image_similar(reference, without_box, 5)
|
||||||
|
|
||||||
def test_formats(self):
|
@pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", ""))
|
||||||
for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]:
|
@pytest.mark.parametrize(
|
||||||
for mode in ["RGB", "L", "RGBA", "LA", "I", ""]:
|
"resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR)
|
||||||
im = hopper(mode)
|
)
|
||||||
box = (20, 20, im.size[0] - 20, im.size[1] - 20)
|
def test_formats(self, mode, resample):
|
||||||
with_box = im.resize((32, 32), resample, box)
|
im = hopper(mode)
|
||||||
cropped = im.crop(box).resize((32, 32), resample)
|
box = (20, 20, im.size[0] - 20, im.size[1] - 20)
|
||||||
assert_image_similar(cropped, with_box, 0.4)
|
with_box = im.resize((32, 32), resample, box)
|
||||||
|
cropped = im.crop(box).resize((32, 32), resample)
|
||||||
|
assert_image_similar(cropped, with_box, 0.4)
|
||||||
|
|
||||||
def test_passthrough(self):
|
def test_passthrough(self):
|
||||||
# When no resize is required
|
# When no resize is required
|
||||||
|
|
|
@ -22,24 +22,15 @@ class TestImagingCoreResize:
|
||||||
im.load()
|
im.load()
|
||||||
return im._new(im.im.resize(size, f))
|
return im._new(im.im.resize(size, f))
|
||||||
|
|
||||||
def test_nearest_mode(self):
|
@pytest.mark.parametrize(
|
||||||
for mode in [
|
"mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16")
|
||||||
"1",
|
)
|
||||||
"P",
|
def test_nearest_mode(self, mode):
|
||||||
"L",
|
im = hopper(mode)
|
||||||
"I",
|
r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
|
||||||
"F",
|
assert r.mode == mode
|
||||||
"RGB",
|
assert r.size == (15, 12)
|
||||||
"RGBA",
|
assert r.im.bands == im.im.bands
|
||||||
"CMYK",
|
|
||||||
"YCbCr",
|
|
||||||
"I;16",
|
|
||||||
]: # exotic mode
|
|
||||||
im = hopper(mode)
|
|
||||||
r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
|
|
||||||
assert r.mode == mode
|
|
||||||
assert r.size == (15, 12)
|
|
||||||
assert r.im.bands == im.im.bands
|
|
||||||
|
|
||||||
def test_convolution_modes(self):
|
def test_convolution_modes(self):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -55,33 +46,58 @@ class TestImagingCoreResize:
|
||||||
assert r.size == (15, 12)
|
assert r.size == (15, 12)
|
||||||
assert r.im.bands == im.im.bands
|
assert r.im.bands == im.im.bands
|
||||||
|
|
||||||
def test_reduce_filters(self):
|
@pytest.mark.parametrize(
|
||||||
for f in [
|
"resample",
|
||||||
|
(
|
||||||
Image.Resampling.NEAREST,
|
Image.Resampling.NEAREST,
|
||||||
Image.Resampling.BOX,
|
Image.Resampling.BOX,
|
||||||
Image.Resampling.BILINEAR,
|
Image.Resampling.BILINEAR,
|
||||||
Image.Resampling.HAMMING,
|
Image.Resampling.HAMMING,
|
||||||
Image.Resampling.BICUBIC,
|
Image.Resampling.BICUBIC,
|
||||||
Image.Resampling.LANCZOS,
|
Image.Resampling.LANCZOS,
|
||||||
]:
|
),
|
||||||
r = self.resize(hopper("RGB"), (15, 12), f)
|
)
|
||||||
assert r.mode == "RGB"
|
def test_reduce_filters(self, resample):
|
||||||
assert r.size == (15, 12)
|
r = self.resize(hopper("RGB"), (15, 12), resample)
|
||||||
|
assert r.mode == "RGB"
|
||||||
|
assert r.size == (15, 12)
|
||||||
|
|
||||||
def test_enlarge_filters(self):
|
@pytest.mark.parametrize(
|
||||||
for f in [
|
"resample",
|
||||||
|
(
|
||||||
Image.Resampling.NEAREST,
|
Image.Resampling.NEAREST,
|
||||||
Image.Resampling.BOX,
|
Image.Resampling.BOX,
|
||||||
Image.Resampling.BILINEAR,
|
Image.Resampling.BILINEAR,
|
||||||
Image.Resampling.HAMMING,
|
Image.Resampling.HAMMING,
|
||||||
Image.Resampling.BICUBIC,
|
Image.Resampling.BICUBIC,
|
||||||
Image.Resampling.LANCZOS,
|
Image.Resampling.LANCZOS,
|
||||||
]:
|
),
|
||||||
r = self.resize(hopper("RGB"), (212, 195), f)
|
)
|
||||||
assert r.mode == "RGB"
|
def test_enlarge_filters(self, resample):
|
||||||
assert r.size == (212, 195)
|
r = self.resize(hopper("RGB"), (212, 195), resample)
|
||||||
|
assert r.mode == "RGB"
|
||||||
|
assert r.size == (212, 195)
|
||||||
|
|
||||||
def test_endianness(self):
|
@pytest.mark.parametrize(
|
||||||
|
"resample",
|
||||||
|
(
|
||||||
|
Image.Resampling.NEAREST,
|
||||||
|
Image.Resampling.BOX,
|
||||||
|
Image.Resampling.BILINEAR,
|
||||||
|
Image.Resampling.HAMMING,
|
||||||
|
Image.Resampling.BICUBIC,
|
||||||
|
Image.Resampling.LANCZOS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode, channels_set",
|
||||||
|
(
|
||||||
|
("RGB", ("blank", "filled", "dirty")),
|
||||||
|
("RGBA", ("blank", "blank", "filled", "dirty")),
|
||||||
|
("LA", ("filled", "dirty")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_endianness(self, resample, mode, channels_set):
|
||||||
# Make an image with one colored pixel, in one channel.
|
# Make an image with one colored pixel, in one channel.
|
||||||
# When resized, that channel should be the same as a GS image.
|
# When resized, that channel should be the same as a GS image.
|
||||||
# Other channels should be unaffected.
|
# Other channels should be unaffected.
|
||||||
|
@ -95,47 +111,37 @@ class TestImagingCoreResize:
|
||||||
}
|
}
|
||||||
samples["dirty"].putpixel((1, 1), 128)
|
samples["dirty"].putpixel((1, 1), 128)
|
||||||
|
|
||||||
for f in [
|
# samples resized with current filter
|
||||||
|
references = {
|
||||||
|
name: self.resize(ch, (4, 4), resample) for name, ch in samples.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
for channels in set(permutations(channels_set)):
|
||||||
|
# compile image from different channels permutations
|
||||||
|
im = Image.merge(mode, [samples[ch] for ch in channels])
|
||||||
|
resized = self.resize(im, (4, 4), resample)
|
||||||
|
|
||||||
|
for i, ch in enumerate(resized.split()):
|
||||||
|
# check what resized channel in image is the same
|
||||||
|
# as separately resized channel
|
||||||
|
assert_image_equal(ch, references[channels[i]])
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"resample",
|
||||||
|
(
|
||||||
Image.Resampling.NEAREST,
|
Image.Resampling.NEAREST,
|
||||||
Image.Resampling.BOX,
|
Image.Resampling.BOX,
|
||||||
Image.Resampling.BILINEAR,
|
Image.Resampling.BILINEAR,
|
||||||
Image.Resampling.HAMMING,
|
Image.Resampling.HAMMING,
|
||||||
Image.Resampling.BICUBIC,
|
Image.Resampling.BICUBIC,
|
||||||
Image.Resampling.LANCZOS,
|
Image.Resampling.LANCZOS,
|
||||||
]:
|
),
|
||||||
# samples resized with current filter
|
)
|
||||||
references = {
|
def test_enlarge_zero(self, resample):
|
||||||
name: self.resize(ch, (4, 4), f) for name, ch in samples.items()
|
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
|
||||||
}
|
assert r.mode == "RGB"
|
||||||
|
assert r.size == (212, 195)
|
||||||
for mode, channels_set in [
|
assert r.getdata()[0] == (0, 0, 0)
|
||||||
("RGB", ("blank", "filled", "dirty")),
|
|
||||||
("RGBA", ("blank", "blank", "filled", "dirty")),
|
|
||||||
("LA", ("filled", "dirty")),
|
|
||||||
]:
|
|
||||||
for channels in set(permutations(channels_set)):
|
|
||||||
# compile image from different channels permutations
|
|
||||||
im = Image.merge(mode, [samples[ch] for ch in channels])
|
|
||||||
resized = self.resize(im, (4, 4), f)
|
|
||||||
|
|
||||||
for i, ch in enumerate(resized.split()):
|
|
||||||
# check what resized channel in image is the same
|
|
||||||
# as separately resized channel
|
|
||||||
assert_image_equal(ch, references[channels[i]])
|
|
||||||
|
|
||||||
def test_enlarge_zero(self):
|
|
||||||
for f in [
|
|
||||||
Image.Resampling.NEAREST,
|
|
||||||
Image.Resampling.BOX,
|
|
||||||
Image.Resampling.BILINEAR,
|
|
||||||
Image.Resampling.HAMMING,
|
|
||||||
Image.Resampling.BICUBIC,
|
|
||||||
Image.Resampling.LANCZOS,
|
|
||||||
]:
|
|
||||||
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f)
|
|
||||||
assert r.mode == "RGB"
|
|
||||||
assert r.size == (212, 195)
|
|
||||||
assert r.getdata()[0] == (0, 0, 0)
|
|
||||||
|
|
||||||
def test_unknown_filter(self):
|
def test_unknown_filter(self):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -179,74 +185,71 @@ class TestReducingGapResize:
|
||||||
(52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99
|
(52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_reducing_gap_1(self, gradients_image):
|
@pytest.mark.parametrize(
|
||||||
for box, epsilon in [
|
"box, epsilon",
|
||||||
(None, 4),
|
((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)),
|
||||||
((1.1, 2.2, 510.8, 510.9), 4),
|
)
|
||||||
((3, 10, 410, 256), 10),
|
def test_reducing_gap_1(self, gradients_image, box, epsilon):
|
||||||
]:
|
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
||||||
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
im = gradients_image.resize(
|
||||||
im = gradients_image.resize(
|
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
|
||||||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
|
)
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
|
||||||
assert_image_equal(ref, im)
|
|
||||||
|
|
||||||
assert_image_similar(ref, im, epsilon)
|
|
||||||
|
|
||||||
def test_reducing_gap_2(self, gradients_image):
|
|
||||||
for box, epsilon in [
|
|
||||||
(None, 1.5),
|
|
||||||
((1.1, 2.2, 510.8, 510.9), 1.5),
|
|
||||||
((3, 10, 410, 256), 1),
|
|
||||||
]:
|
|
||||||
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
|
||||||
im = gradients_image.resize(
|
|
||||||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
|
||||||
assert_image_equal(ref, im)
|
|
||||||
|
|
||||||
assert_image_similar(ref, im, epsilon)
|
|
||||||
|
|
||||||
def test_reducing_gap_3(self, gradients_image):
|
|
||||||
for box, epsilon in [
|
|
||||||
(None, 1),
|
|
||||||
((1.1, 2.2, 510.8, 510.9), 1),
|
|
||||||
((3, 10, 410, 256), 0.5),
|
|
||||||
]:
|
|
||||||
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
|
||||||
im = gradients_image.resize(
|
|
||||||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
|
||||||
assert_image_equal(ref, im)
|
|
||||||
|
|
||||||
assert_image_similar(ref, im, epsilon)
|
|
||||||
|
|
||||||
def test_reducing_gap_8(self, gradients_image):
|
|
||||||
for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]:
|
|
||||||
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
|
||||||
im = gradients_image.resize(
|
|
||||||
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
|
|
||||||
)
|
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
assert_image_equal(ref, im)
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
def test_box_filter(self, gradients_image):
|
assert_image_similar(ref, im, epsilon)
|
||||||
for box, epsilon in [
|
|
||||||
((0, 0, 512, 512), 5.5),
|
|
||||||
((0.9, 1.7, 128, 128), 9.5),
|
|
||||||
]:
|
|
||||||
ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
|
|
||||||
im = gradients_image.resize(
|
|
||||||
(52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
|
|
||||||
)
|
|
||||||
|
|
||||||
assert_image_similar(ref, im, epsilon)
|
@pytest.mark.parametrize(
|
||||||
|
"box, epsilon",
|
||||||
|
((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)),
|
||||||
|
)
|
||||||
|
def test_reducing_gap_2(self, gradients_image, box, epsilon):
|
||||||
|
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
||||||
|
im = gradients_image.resize(
|
||||||
|
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
assert_image_similar(ref, im, epsilon)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"box, epsilon",
|
||||||
|
((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)),
|
||||||
|
)
|
||||||
|
def test_reducing_gap_3(self, gradients_image, box, epsilon):
|
||||||
|
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
||||||
|
im = gradients_image.resize(
|
||||||
|
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
assert_image_similar(ref, im, epsilon)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)))
|
||||||
|
def test_reducing_gap_8(self, gradients_image, box):
|
||||||
|
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
|
||||||
|
im = gradients_image.resize(
|
||||||
|
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_image_equal(ref, im)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"box, epsilon",
|
||||||
|
(((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)),
|
||||||
|
)
|
||||||
|
def test_box_filter(self, gradients_image, box, epsilon):
|
||||||
|
ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
|
||||||
|
im = gradients_image.resize(
|
||||||
|
(52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_image_similar(ref, im, epsilon)
|
||||||
|
|
||||||
|
|
||||||
class TestImageResize:
|
class TestImageResize:
|
||||||
|
@ -273,15 +276,14 @@ class TestImageResize:
|
||||||
im = im.resize((64, 64))
|
im = im.resize((64, 64))
|
||||||
assert im.size == (64, 64)
|
assert im.size == (64, 64)
|
||||||
|
|
||||||
def test_default_filter(self):
|
@pytest.mark.parametrize("mode", ("L", "RGB", "I", "F"))
|
||||||
for mode in "L", "RGB", "I", "F":
|
def test_default_filter_bicubic(self, mode):
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
|
assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
|
||||||
|
|
||||||
for mode in "1", "P":
|
@pytest.mark.parametrize(
|
||||||
im = hopper(mode)
|
"mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16")
|
||||||
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
|
)
|
||||||
|
def test_default_filter_nearest(self, mode):
|
||||||
for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16":
|
im = hopper(mode)
|
||||||
im = hopper(mode)
|
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
|
||||||
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
|
@ -22,26 +24,26 @@ def rotate(im, mode, angle, center=None, translate=None):
|
||||||
assert out.size != im.size
|
assert out.size != im.size
|
||||||
|
|
||||||
|
|
||||||
def test_mode():
|
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
|
||||||
for mode in ("1", "P", "L", "RGB", "I", "F"):
|
def test_mode(mode):
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
rotate(im, mode, 45)
|
rotate(im, mode, 45)
|
||||||
|
|
||||||
|
|
||||||
def test_angle():
|
@pytest.mark.parametrize("angle", (0, 90, 180, 270))
|
||||||
for angle in (0, 90, 180, 270):
|
def test_angle(angle):
|
||||||
with Image.open("Tests/images/test-card.png") as im:
|
with Image.open("Tests/images/test-card.png") as im:
|
||||||
rotate(im, im.mode, angle)
|
|
||||||
|
|
||||||
im = hopper()
|
|
||||||
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
|
|
||||||
|
|
||||||
|
|
||||||
def test_zero():
|
|
||||||
for angle in (0, 45, 90, 180, 270):
|
|
||||||
im = Image.new("RGB", (0, 0))
|
|
||||||
rotate(im, im.mode, angle)
|
rotate(im, im.mode, angle)
|
||||||
|
|
||||||
|
im = hopper()
|
||||||
|
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
|
||||||
|
def test_zero(angle):
|
||||||
|
im = Image.new("RGB", (0, 0))
|
||||||
|
rotate(im, im.mode, angle)
|
||||||
|
|
||||||
|
|
||||||
def test_resample():
|
def test_resample():
|
||||||
# Target image creation, inspected by eye.
|
# Target image creation, inspected by eye.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL.Image import Transpose
|
from PIL.Image import Transpose
|
||||||
|
|
||||||
from . import helper
|
from . import helper
|
||||||
|
@ -9,157 +11,136 @@ HOPPER = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_flip_left_right():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
def transpose(mode):
|
def test_flip_left_right(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
|
out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size
|
assert out.size == im.size
|
||||||
|
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1))
|
assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1))
|
||||||
assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
|
assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
|
||||||
assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2))
|
assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2))
|
||||||
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2))
|
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2))
|
||||||
|
|
||||||
for mode in HOPPER:
|
|
||||||
transpose(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_flip_top_bottom():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
def transpose(mode):
|
def test_flip_top_bottom(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
|
out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size
|
assert out.size == im.size
|
||||||
|
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
assert im.getpixel((1, 1)) == out.getpixel((1, y - 2))
|
assert im.getpixel((1, 1)) == out.getpixel((1, y - 2))
|
||||||
assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2))
|
assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2))
|
||||||
assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
|
assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
|
||||||
assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1))
|
assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1))
|
||||||
|
|
||||||
for mode in HOPPER:
|
|
||||||
transpose(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_rotate_90():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
def transpose(mode):
|
def test_rotate_90(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
out = im.transpose(Transpose.ROTATE_90)
|
out = im.transpose(Transpose.ROTATE_90)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size[::-1]
|
assert out.size == im.size[::-1]
|
||||||
|
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
assert im.getpixel((1, 1)) == out.getpixel((1, x - 2))
|
assert im.getpixel((1, 1)) == out.getpixel((1, x - 2))
|
||||||
assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
|
assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
|
||||||
assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2))
|
assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2))
|
||||||
assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1))
|
assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1))
|
||||||
|
|
||||||
for mode in HOPPER:
|
|
||||||
transpose(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_rotate_180():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
def transpose(mode):
|
def test_rotate_180(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
out = im.transpose(Transpose.ROTATE_180)
|
out = im.transpose(Transpose.ROTATE_180)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size
|
assert out.size == im.size
|
||||||
|
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2))
|
assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2))
|
||||||
assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2))
|
assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2))
|
||||||
assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1))
|
assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1))
|
||||||
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
|
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
|
||||||
|
|
||||||
for mode in HOPPER:
|
|
||||||
transpose(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_rotate_270():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
def transpose(mode):
|
def test_rotate_270(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
out = im.transpose(Transpose.ROTATE_270)
|
out = im.transpose(Transpose.ROTATE_270)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size[::-1]
|
assert out.size == im.size[::-1]
|
||||||
|
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1))
|
assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1))
|
||||||
assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2))
|
assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2))
|
||||||
assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
|
assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
|
||||||
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2))
|
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2))
|
||||||
|
|
||||||
for mode in HOPPER:
|
|
||||||
transpose(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_transpose():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
def transpose(mode):
|
def test_transpose(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
out = im.transpose(Transpose.TRANSPOSE)
|
out = im.transpose(Transpose.TRANSPOSE)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size[::-1]
|
assert out.size == im.size[::-1]
|
||||||
|
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
assert im.getpixel((1, 1)) == out.getpixel((1, 1))
|
assert im.getpixel((1, 1)) == out.getpixel((1, 1))
|
||||||
assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2))
|
assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2))
|
||||||
assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1))
|
assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1))
|
||||||
assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2))
|
assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2))
|
||||||
|
|
||||||
for mode in HOPPER:
|
|
||||||
transpose(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tranverse():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
def transpose(mode):
|
def test_tranverse(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
out = im.transpose(Transpose.TRANSVERSE)
|
out = im.transpose(Transpose.TRANSVERSE)
|
||||||
assert out.mode == mode
|
assert out.mode == mode
|
||||||
assert out.size == im.size[::-1]
|
assert out.size == im.size[::-1]
|
||||||
|
|
||||||
x, y = im.size
|
x, y = im.size
|
||||||
assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2))
|
assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2))
|
||||||
assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1))
|
assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1))
|
||||||
assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2))
|
assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2))
|
||||||
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
|
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
|
||||||
|
|
||||||
for mode in HOPPER:
|
|
||||||
transpose(mode)
|
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip():
|
@pytest.mark.parametrize("mode", HOPPER)
|
||||||
for mode in HOPPER:
|
def test_roundtrip(mode):
|
||||||
im = HOPPER[mode]
|
im = HOPPER[mode]
|
||||||
|
|
||||||
def transpose(first, second):
|
def transpose(first, second):
|
||||||
return im.transpose(first).transpose(second)
|
return im.transpose(first).transpose(second)
|
||||||
|
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT)
|
im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT)
|
||||||
)
|
)
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM)
|
im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM)
|
||||||
)
|
)
|
||||||
assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270))
|
assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270))
|
||||||
assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180))
|
assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180))
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im.transpose(Transpose.TRANSPOSE),
|
im.transpose(Transpose.TRANSPOSE),
|
||||||
transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM),
|
transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM),
|
||||||
)
|
)
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im.transpose(Transpose.TRANSPOSE),
|
im.transpose(Transpose.TRANSPOSE),
|
||||||
transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT),
|
transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT),
|
||||||
)
|
)
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im.transpose(Transpose.TRANSVERSE),
|
im.transpose(Transpose.TRANSVERSE),
|
||||||
transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT),
|
transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT),
|
||||||
)
|
)
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im.transpose(Transpose.TRANSVERSE),
|
im.transpose(Transpose.TRANSVERSE),
|
||||||
transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM),
|
transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM),
|
||||||
)
|
)
|
||||||
assert_image_equal(
|
assert_image_equal(
|
||||||
im.transpose(Transpose.TRANSVERSE),
|
im.transpose(Transpose.TRANSVERSE),
|
||||||
transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE),
|
transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE),
|
||||||
)
|
)
|
||||||
|
|
|
@ -174,19 +174,24 @@ def test_exceptions():
|
||||||
psRGB = ImageCms.createProfile("sRGB")
|
psRGB = ImageCms.createProfile("sRGB")
|
||||||
pLab = ImageCms.createProfile("LAB")
|
pLab = ImageCms.createProfile("LAB")
|
||||||
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
|
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError, match="mode mismatch"):
|
||||||
t.apply_in_place(hopper("RGBA"))
|
t.apply_in_place(hopper("RGBA"))
|
||||||
|
|
||||||
# the procedural pyCMS API uses PyCMSError for all sorts of errors
|
# the procedural pyCMS API uses PyCMSError for all sorts of errors
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
with pytest.raises(ImageCms.PyCMSError):
|
with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"):
|
||||||
ImageCms.profileToProfile(im, "foo", "bar")
|
ImageCms.profileToProfile(im, "foo", "bar")
|
||||||
with pytest.raises(ImageCms.PyCMSError):
|
|
||||||
|
with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"):
|
||||||
ImageCms.buildTransform("foo", "bar", "RGB", "RGB")
|
ImageCms.buildTransform("foo", "bar", "RGB", "RGB")
|
||||||
with pytest.raises(ImageCms.PyCMSError):
|
|
||||||
|
with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"):
|
||||||
ImageCms.getProfileName(None)
|
ImageCms.getProfileName(None)
|
||||||
skip_missing()
|
skip_missing()
|
||||||
with pytest.raises(ImageCms.PyCMSError):
|
|
||||||
|
# Python <= 3.9: "an integer is required (got type NoneType)"
|
||||||
|
# Python > 3.9: "'NoneType' object cannot be interpreted as an integer"
|
||||||
|
with pytest.raises(ImageCms.PyCMSError, match="integer"):
|
||||||
ImageCms.isIntentSupported(SRGB, None, None)
|
ImageCms.isIntentSupported(SRGB, None, None)
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,15 +206,32 @@ def test_lab_color_profile():
|
||||||
|
|
||||||
|
|
||||||
def test_unsupported_color_space():
|
def test_unsupported_color_space():
|
||||||
with pytest.raises(ImageCms.PyCMSError):
|
with pytest.raises(
|
||||||
|
ImageCms.PyCMSError,
|
||||||
|
match=re.escape(
|
||||||
|
"Color space not supported for on-the-fly profile creation (unsupported)"
|
||||||
|
),
|
||||||
|
):
|
||||||
ImageCms.createProfile("unsupported")
|
ImageCms.createProfile("unsupported")
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_color_temperature():
|
def test_invalid_color_temperature():
|
||||||
with pytest.raises(ImageCms.PyCMSError):
|
with pytest.raises(
|
||||||
|
ImageCms.PyCMSError,
|
||||||
|
match='Color temperature must be numeric, "invalid" not valid',
|
||||||
|
):
|
||||||
ImageCms.createProfile("LAB", "invalid")
|
ImageCms.createProfile("LAB", "invalid")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("flag", ("my string", -1))
|
||||||
|
def test_invalid_flag(flag):
|
||||||
|
with hopper() as im:
|
||||||
|
with pytest.raises(
|
||||||
|
ImageCms.PyCMSError, match="flags must be an integer between 0 and "
|
||||||
|
):
|
||||||
|
ImageCms.profileToProfile(im, "foo", "bar", flags=flag)
|
||||||
|
|
||||||
|
|
||||||
def test_simple_lab():
|
def test_simple_lab():
|
||||||
i = Image.new("RGB", (10, 10), (128, 128, 128))
|
i = Image.new("RGB", (10, 10), (128, 128, 128))
|
||||||
|
|
||||||
|
@ -461,9 +483,9 @@ def test_profile_typesafety():
|
||||||
prepatch, these would segfault, postpatch they should emit a typeerror
|
prepatch, these would segfault, postpatch they should emit a typeerror
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError, match="Invalid type for Profile"):
|
||||||
ImageCms.ImageCmsProfile(0).tobytes()
|
ImageCms.ImageCmsProfile(0).tobytes()
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError, match="Invalid type for Profile"):
|
||||||
ImageCms.ImageCmsProfile(1).tobytes()
|
ImageCms.ImageCmsProfile(1).tobytes()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -625,20 +625,20 @@ def test_polygon2():
|
||||||
helper_polygon(POINTS2)
|
helper_polygon(POINTS2)
|
||||||
|
|
||||||
|
|
||||||
def test_polygon_kite():
|
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
||||||
|
def test_polygon_kite(mode):
|
||||||
# Test drawing lines of different gradients (dx>dy, dy>dx) and
|
# Test drawing lines of different gradients (dx>dy, dy>dx) and
|
||||||
# vertical (dx==0) and horizontal (dy==0) lines
|
# vertical (dx==0) and horizontal (dy==0) lines
|
||||||
for mode in ["RGB", "L"]:
|
# Arrange
|
||||||
# Arrange
|
im = Image.new(mode, (W, H))
|
||||||
im = Image.new(mode, (W, H))
|
draw = ImageDraw.Draw(im)
|
||||||
draw = ImageDraw.Draw(im)
|
expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png"
|
||||||
expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png"
|
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
draw.polygon(KITE_POINTS, fill="blue", outline="yellow")
|
draw.polygon(KITE_POINTS, fill="blue", outline="yellow")
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert_image_equal_tofile(im, expected)
|
assert_image_equal_tofile(im, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_polygon_1px_high():
|
def test_polygon_1px_high():
|
||||||
|
@ -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))
|
||||||
|
@ -1218,21 +1232,39 @@ def test_textsize_empty_string():
|
||||||
# Act
|
# Act
|
||||||
# Should not cause 'SystemError: <built-in method getsize of
|
# Should not cause 'SystemError: <built-in method getsize of
|
||||||
# ImagingFont object at 0x...> returned NULL without setting an error'
|
# ImagingFont object at 0x...> returned NULL without setting an error'
|
||||||
draw.textsize("")
|
draw.textbbox((0, 0), "")
|
||||||
draw.textsize("\n")
|
draw.textbbox((0, 0), "\n")
|
||||||
draw.textsize("test\n")
|
draw.textbbox((0, 0), "test\n")
|
||||||
|
draw.textlength("")
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("freetype2")
|
@skip_unless_feature("freetype2")
|
||||||
def test_textsize_stroke():
|
def test_textbbox_stroke():
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
|
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert draw.textsize("A", font, stroke_width=2) == (16, 20)
|
assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20)
|
||||||
assert draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) == (52, 44)
|
assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22)
|
||||||
|
assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44)
|
||||||
|
assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50)
|
||||||
|
|
||||||
|
|
||||||
|
def test_textsize_deprecation():
|
||||||
|
im = Image.new("RGB", (W, H))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
draw.textsize("Hello")
|
||||||
|
assert len(log) == 1
|
||||||
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
draw.textsize("Hello\nWorld")
|
||||||
|
assert len(log) == 1
|
||||||
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
draw.multiline_textsize("Hello\nWorld")
|
||||||
|
assert len(log) == 1
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("freetype2")
|
@skip_unless_feature("freetype2")
|
||||||
|
@ -1282,6 +1314,23 @@ def test_stroke_multiline():
|
||||||
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3)
|
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_setting_default_font():
|
||||||
|
# Arrange
|
||||||
|
im = Image.new("RGB", (100, 250))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
ImageDraw.ImageDraw.font = font
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
try:
|
||||||
|
assert draw.getfont() == font
|
||||||
|
finally:
|
||||||
|
ImageDraw.ImageDraw.font = None
|
||||||
|
assert isinstance(draw.getfont(), ImageFont.ImageFont)
|
||||||
|
|
||||||
|
|
||||||
def test_same_color_outline():
|
def test_same_color_outline():
|
||||||
# Prepare shape
|
# Prepare shape
|
||||||
x0, y0 = 5, 5
|
x0, y0 = 5, 5
|
||||||
|
@ -1452,3 +1501,11 @@ def test_discontiguous_corners_polygon():
|
||||||
)
|
)
|
||||||
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
||||||
assert_image_similar_tofile(img, expected, 1)
|
assert_image_similar_tofile(img, expected, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_polygon():
|
||||||
|
im = Image.new("RGB", (W, H))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
|
||||||
|
expected = "Tests/images/imagedraw_outline_polygon_RGB.png"
|
||||||
|
assert_image_similar_tofile(im, expected, 1)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageDraw2
|
from PIL import Image, ImageDraw, ImageDraw2
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
|
@ -205,7 +207,9 @@ def test_textsize():
|
||||||
font = ImageDraw2.Font("white", FONT_PATH)
|
font = ImageDraw2.Font("white", FONT_PATH)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
size = draw.textsize("ImageDraw2", font)
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
size = draw.textsize("ImageDraw2", font)
|
||||||
|
assert len(log) == 1
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert size[1] == 12
|
assert size[1] == 12
|
||||||
|
@ -221,9 +225,10 @@ def test_textsize_empty_string():
|
||||||
# Act
|
# Act
|
||||||
# Should not cause 'SystemError: <built-in method getsize of
|
# Should not cause 'SystemError: <built-in method getsize of
|
||||||
# ImagingFont object at 0x...> returned NULL without setting an error'
|
# ImagingFont object at 0x...> returned NULL without setting an error'
|
||||||
draw.textsize("", font)
|
draw.textbbox((0, 0), "", font)
|
||||||
draw.textsize("\n", font)
|
draw.textbbox((0, 0), "\n", font)
|
||||||
draw.textsize("test\n", font)
|
draw.textbbox((0, 0), "test\n", font)
|
||||||
|
draw.textlength("", font)
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("freetype2")
|
@skip_unless_feature("freetype2")
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -91,7 +94,7 @@ class TestImageFont:
|
||||||
def _render(self, font):
|
def _render(self, font):
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE)
|
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE)
|
||||||
ttf.getsize(txt)
|
ttf.getbbox(txt)
|
||||||
|
|
||||||
img = Image.new("RGB", (256, 64), "white")
|
img = Image.new("RGB", (256, 64), "white")
|
||||||
d = ImageDraw.Draw(img)
|
d = ImageDraw.Draw(img)
|
||||||
|
@ -132,15 +135,15 @@ class TestImageFont:
|
||||||
target = "Tests/images/transparent_background_text_L.png"
|
target = "Tests/images/transparent_background_text_L.png"
|
||||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
||||||
def test_textsize_equal(self):
|
def test_textbbox_equal(self):
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
ttf = self.get_font()
|
ttf = self.get_font()
|
||||||
|
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
size = draw.textsize(txt, ttf)
|
bbox = draw.textbbox((10, 10), txt, ttf)
|
||||||
draw.text((10, 10), txt, font=ttf)
|
draw.text((10, 10), txt, font=ttf)
|
||||||
draw.rectangle((10, 10, 10 + size[0], 10 + size[1]))
|
draw.rectangle(bbox)
|
||||||
|
|
||||||
assert_image_similar_tofile(
|
assert_image_similar_tofile(
|
||||||
im, "Tests/images/rectangle_surrounding_text.png", 2.5
|
im, "Tests/images/rectangle_surrounding_text.png", 2.5
|
||||||
|
@ -181,7 +184,7 @@ class TestImageFont:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
ttf = self.get_font()
|
ttf = self.get_font()
|
||||||
line_spacing = draw.textsize("A", font=ttf)[1] + 4
|
line_spacing = ttf.getbbox("A")[3] + 4
|
||||||
lines = TEST_TEXT.split("\n")
|
lines = TEST_TEXT.split("\n")
|
||||||
y = 0
|
y = 0
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
@ -242,19 +245,39 @@ class TestImageFont:
|
||||||
im = Image.new(mode="RGB", size=(300, 100))
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
# Test that textsize() correctly connects to multiline_textsize()
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize(
|
# Test that textsize() correctly connects to multiline_textsize()
|
||||||
TEST_TEXT, font=ttf
|
assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize(
|
||||||
|
TEST_TEXT, font=ttf
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that multiline_textsize corresponds to ImageFont.textsize()
|
||||||
|
# for single line text
|
||||||
|
assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf)
|
||||||
|
|
||||||
|
# Test that textsize() can pass on additional arguments
|
||||||
|
# to multiline_textsize()
|
||||||
|
draw.textsize(TEST_TEXT, font=ttf, spacing=4)
|
||||||
|
draw.textsize(TEST_TEXT, ttf, 4)
|
||||||
|
assert len(log) == 6
|
||||||
|
|
||||||
|
def test_multiline_bbox(self):
|
||||||
|
ttf = self.get_font()
|
||||||
|
im = Image.new(mode="RGB", size=(300, 100))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
# Test that textbbox() correctly connects to multiline_textbbox()
|
||||||
|
assert draw.textbbox((0, 0), TEST_TEXT, font=ttf) == draw.multiline_textbbox(
|
||||||
|
(0, 0), TEST_TEXT, font=ttf
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test that multiline_textsize corresponds to ImageFont.textsize()
|
# Test that multiline_textbbox corresponds to ImageFont.textbbox()
|
||||||
# for single line text
|
# for single line text
|
||||||
assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf)
|
assert ttf.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=ttf)
|
||||||
|
|
||||||
# Test that textsize() can pass on additional arguments
|
# Test that textbbox() can pass on additional arguments
|
||||||
# to multiline_textsize()
|
# to multiline_textbbox()
|
||||||
draw.textsize(TEST_TEXT, font=ttf, spacing=4)
|
draw.textbbox((0, 0), TEST_TEXT, font=ttf, spacing=4)
|
||||||
draw.textsize(TEST_TEXT, ttf, 4)
|
|
||||||
|
|
||||||
def test_multiline_width(self):
|
def test_multiline_width(self):
|
||||||
ttf = self.get_font()
|
ttf = self.get_font()
|
||||||
|
@ -262,9 +285,15 @@ class TestImageFont:
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
draw.textsize("longest line", font=ttf)[0]
|
draw.textbbox((0, 0), "longest line", font=ttf)[2]
|
||||||
== draw.multiline_textsize("longest line\nline", font=ttf)[0]
|
== draw.multiline_textbbox((0, 0), "longest line\nline", font=ttf)[2]
|
||||||
)
|
)
|
||||||
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
assert (
|
||||||
|
draw.textsize("longest line", font=ttf)[0]
|
||||||
|
== draw.multiline_textsize("longest line\nline", font=ttf)[0]
|
||||||
|
)
|
||||||
|
assert len(log) == 2
|
||||||
|
|
||||||
def test_multiline_spacing(self):
|
def test_multiline_spacing(self):
|
||||||
ttf = self.get_font()
|
ttf = self.get_font()
|
||||||
|
@ -286,16 +315,33 @@ class TestImageFont:
|
||||||
|
|
||||||
# Original font
|
# Original font
|
||||||
draw.font = font
|
draw.font = font
|
||||||
box_size_a = draw.textsize(word)
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
box_size_a = draw.textsize(word)
|
||||||
|
assert box_size_a == font.getsize(word)
|
||||||
|
assert len(log) == 2
|
||||||
|
bbox_a = draw.textbbox((10, 10), word)
|
||||||
|
|
||||||
# Rotated font
|
# Rotated font
|
||||||
draw.font = transposed_font
|
draw.font = transposed_font
|
||||||
box_size_b = draw.textsize(word)
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
box_size_b = draw.textsize(word)
|
||||||
|
assert box_size_b == transposed_font.getsize(word)
|
||||||
|
assert len(log) == 2
|
||||||
|
bbox_b = draw.textbbox((20, 20), word)
|
||||||
|
|
||||||
# Check (w,h) of box a is (h,w) of box b
|
# Check (w,h) of box a is (h,w) of box b
|
||||||
assert box_size_a[0] == box_size_b[1]
|
assert box_size_a[0] == box_size_b[1]
|
||||||
assert box_size_a[1] == box_size_b[0]
|
assert box_size_a[1] == box_size_b[0]
|
||||||
|
|
||||||
|
# Check bbox b is (20, 20, 20 + h, 20 + w)
|
||||||
|
assert bbox_b[0] == 20
|
||||||
|
assert bbox_b[1] == 20
|
||||||
|
assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1]
|
||||||
|
assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0]
|
||||||
|
|
||||||
|
# text length is undefined for vertical text
|
||||||
|
pytest.raises(ValueError, draw.textlength, word)
|
||||||
|
|
||||||
def test_unrotated_transposed_font(self):
|
def test_unrotated_transposed_font(self):
|
||||||
img_grey = Image.new("L", (100, 100))
|
img_grey = Image.new("L", (100, 100))
|
||||||
draw = ImageDraw.Draw(img_grey)
|
draw = ImageDraw.Draw(img_grey)
|
||||||
|
@ -307,15 +353,31 @@ class TestImageFont:
|
||||||
|
|
||||||
# Original font
|
# Original font
|
||||||
draw.font = font
|
draw.font = font
|
||||||
box_size_a = draw.textsize(word)
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
box_size_a = draw.textsize(word)
|
||||||
|
assert len(log) == 1
|
||||||
|
bbox_a = draw.textbbox((10, 10), word)
|
||||||
|
length_a = draw.textlength(word)
|
||||||
|
|
||||||
# Rotated font
|
# Rotated font
|
||||||
draw.font = transposed_font
|
draw.font = transposed_font
|
||||||
box_size_b = draw.textsize(word)
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
box_size_b = draw.textsize(word)
|
||||||
|
assert len(log) == 1
|
||||||
|
bbox_b = draw.textbbox((20, 20), word)
|
||||||
|
length_b = draw.textlength(word)
|
||||||
|
|
||||||
# Check boxes a and b are same size
|
# Check boxes a and b are same size
|
||||||
assert box_size_a == box_size_b
|
assert box_size_a == box_size_b
|
||||||
|
|
||||||
|
# Check bbox b is (20, 20, 20 + w, 20 + h)
|
||||||
|
assert bbox_b[0] == 20
|
||||||
|
assert bbox_b[1] == 20
|
||||||
|
assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0]
|
||||||
|
assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1]
|
||||||
|
|
||||||
|
assert length_a == length_b
|
||||||
|
|
||||||
def test_rotated_transposed_font_get_mask(self):
|
def test_rotated_transposed_font_get_mask(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
text = "mask this"
|
text = "mask this"
|
||||||
|
@ -370,9 +432,11 @@ class TestImageFont:
|
||||||
text = "offset this"
|
text = "offset this"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
offset = font.getoffset(text)
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
offset = font.getoffset(text)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
assert len(log) == 1
|
||||||
assert offset == (0, 3)
|
assert offset == (0, 3)
|
||||||
|
|
||||||
def test_free_type_font_get_mask(self):
|
def test_free_type_font_get_mask(self):
|
||||||
|
@ -414,11 +478,11 @@ class TestImageFont:
|
||||||
# Assert
|
# Assert
|
||||||
assert_image_equal_tofile(im, "Tests/images/default_font.png")
|
assert_image_equal_tofile(im, "Tests/images/default_font.png")
|
||||||
|
|
||||||
def test_getsize_empty(self):
|
def test_getbbox_empty(self):
|
||||||
# issue #2614
|
# issue #2614
|
||||||
font = self.get_font()
|
font = self.get_font()
|
||||||
# should not crash.
|
# should not crash.
|
||||||
assert (0, 0) == font.getsize("")
|
assert (0, 0, 0, 0) == font.getbbox("")
|
||||||
|
|
||||||
def test_render_empty(self):
|
def test_render_empty(self):
|
||||||
# issue 2666
|
# issue 2666
|
||||||
|
@ -435,7 +499,7 @@ class TestImageFont:
|
||||||
# issue #2826
|
# issue #2826
|
||||||
font = ImageFont.load_default()
|
font = ImageFont.load_default()
|
||||||
with pytest.raises(UnicodeEncodeError):
|
with pytest.raises(UnicodeEncodeError):
|
||||||
font.getsize("’")
|
font.getbbox("’")
|
||||||
|
|
||||||
def test_unicode_extended(self):
|
def test_unicode_extended(self):
|
||||||
# issue #3777
|
# issue #3777
|
||||||
|
@ -560,17 +624,29 @@ class TestImageFont:
|
||||||
assert t.font.x_ppem == 20
|
assert t.font.x_ppem == 20
|
||||||
assert t.font.y_ppem == 20
|
assert t.font.y_ppem == 20
|
||||||
assert t.font.glyphs == 4177
|
assert t.font.glyphs == 4177
|
||||||
assert t.getsize("A") == (12, 16)
|
assert t.getbbox("A") == (0, 4, 12, 16)
|
||||||
assert t.getsize("AB") == (24, 16)
|
assert t.getbbox("AB") == (0, 4, 24, 16)
|
||||||
assert t.getsize("M") == (12, 16)
|
assert t.getbbox("M") == (0, 4, 12, 16)
|
||||||
assert t.getsize("y") == (12, 20)
|
assert t.getbbox("y") == (0, 7, 12, 20)
|
||||||
assert t.getsize("a") == (12, 16)
|
assert t.getbbox("a") == (0, 7, 12, 16)
|
||||||
assert t.getsize_multiline("A") == (12, 16)
|
assert t.getlength("A") == 12
|
||||||
assert t.getsize_multiline("AB") == (24, 16)
|
assert t.getlength("AB") == 24
|
||||||
assert t.getsize_multiline("a") == (12, 16)
|
assert t.getlength("M") == 12
|
||||||
assert t.getsize_multiline("ABC\n") == (36, 36)
|
assert t.getlength("y") == 12
|
||||||
assert t.getsize_multiline("ABC\nA") == (36, 36)
|
assert t.getlength("a") == 12
|
||||||
assert t.getsize_multiline("ABC\nAaaa") == (48, 36)
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
assert t.getsize("A") == (12, 16)
|
||||||
|
assert t.getsize("AB") == (24, 16)
|
||||||
|
assert t.getsize("M") == (12, 16)
|
||||||
|
assert t.getsize("y") == (12, 20)
|
||||||
|
assert t.getsize("a") == (12, 16)
|
||||||
|
assert t.getsize_multiline("A") == (12, 16)
|
||||||
|
assert t.getsize_multiline("AB") == (24, 16)
|
||||||
|
assert t.getsize_multiline("a") == (12, 16)
|
||||||
|
assert t.getsize_multiline("ABC\n") == (36, 36)
|
||||||
|
assert t.getsize_multiline("ABC\nA") == (36, 36)
|
||||||
|
assert t.getsize_multiline("ABC\nAaaa") == (48, 36)
|
||||||
|
assert len(log) == 11
|
||||||
|
|
||||||
def test_getsize_stroke(self):
|
def test_getsize_stroke(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
|
@ -578,14 +654,22 @@ class TestImageFont:
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
for stroke_width in [0, 2]:
|
for stroke_width in [0, 2]:
|
||||||
assert t.getsize("A", stroke_width=stroke_width) == (
|
assert t.getbbox("A", stroke_width=stroke_width) == (
|
||||||
12 + stroke_width * 2,
|
0 - stroke_width,
|
||||||
16 + stroke_width * 2,
|
4 - stroke_width,
|
||||||
)
|
12 + stroke_width,
|
||||||
assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == (
|
16 + stroke_width,
|
||||||
48 + stroke_width * 2,
|
|
||||||
36 + stroke_width * 4,
|
|
||||||
)
|
)
|
||||||
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
|
assert t.getsize("A", stroke_width=stroke_width) == (
|
||||||
|
12 + stroke_width * 2,
|
||||||
|
16 + stroke_width * 2,
|
||||||
|
)
|
||||||
|
assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == (
|
||||||
|
48 + stroke_width * 2,
|
||||||
|
36 + stroke_width * 4,
|
||||||
|
)
|
||||||
|
assert len(log) == 2
|
||||||
|
|
||||||
def test_complex_font_settings(self):
|
def test_complex_font_settings(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
|
@ -717,8 +801,11 @@ class TestImageFont:
|
||||||
im = Image.new("RGB", (200, 200))
|
im = Image.new("RGB", (200, 200))
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
default_font = ImageFont.load_default()
|
default_font = ImageFont.load_default()
|
||||||
with pytest.raises(ValueError):
|
with pytest.warns(DeprecationWarning) as log:
|
||||||
d.textbbox((0, 0), "test", font=default_font)
|
width, height = d.textsize("test", font=default_font)
|
||||||
|
assert len(log) == 1
|
||||||
|
assert d.textlength("test", font=default_font) == width
|
||||||
|
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"anchor, left, top",
|
"anchor, left, top",
|
||||||
|
@ -865,7 +952,7 @@ class TestImageFont:
|
||||||
def test_standard_embedded_color(self):
|
def test_standard_embedded_color(self):
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE)
|
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE)
|
||||||
ttf.getsize(txt)
|
ttf.getbbox(txt)
|
||||||
|
|
||||||
im = Image.new("RGB", (300, 64), "white")
|
im = Image.new("RGB", (300, 64), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
|
|
|
@ -140,8 +140,8 @@ def test_ligature_features():
|
||||||
target = "Tests/images/test_ligature_features.png"
|
target = "Tests/images/test_ligature_features.png"
|
||||||
assert_image_similar_tofile(im, target, 0.5)
|
assert_image_similar_tofile(im, target, 0.5)
|
||||||
|
|
||||||
liga_size = ttf.getsize("fi", features=["-liga"])
|
liga_bbox = ttf.getbbox("fi", features=["-liga"])
|
||||||
assert liga_size == (13, 19)
|
assert liga_bbox == (0, 4, 13, 19)
|
||||||
|
|
||||||
|
|
||||||
def test_kerning_features():
|
def test_kerning_features():
|
||||||
|
|
|
@ -345,11 +345,15 @@ def test_exif_transpose():
|
||||||
check(orientation_im)
|
check(orientation_im)
|
||||||
|
|
||||||
# Orientation from "XML:com.adobe.xmp" info key
|
# Orientation from "XML:com.adobe.xmp" info key
|
||||||
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
for suffix in ("", "_exiftool"):
|
||||||
assert im.getexif()[0x0112] == 3
|
with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im:
|
||||||
|
assert im.getexif()[0x0112] == 3
|
||||||
|
|
||||||
transposed_im = ImageOps.exif_transpose(im)
|
transposed_im = ImageOps.exif_transpose(im)
|
||||||
assert 0x0112 not in transposed_im.getexif()
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
|
transposed_im._reload_exif()
|
||||||
|
assert 0x0112 not in transposed_im.getexif()
|
||||||
|
|
||||||
# Orientation from "Raw profile type exif" info key
|
# Orientation from "Raw profile type exif" info key
|
||||||
# This test image has been manually hexedited from exif_imagemagick.png
|
# This test image has been manually hexedited from exif_imagemagick.png
|
||||||
|
|
|
@ -16,32 +16,32 @@ if ImageQt.qt_is_installed:
|
||||||
from PIL.ImageQt import QImage
|
from PIL.ImageQt import QImage
|
||||||
|
|
||||||
|
|
||||||
def test_sanity(tmp_path):
|
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
|
||||||
for mode in ("RGB", "RGBA", "L", "P", "1"):
|
def test_sanity(mode, tmp_path):
|
||||||
src = hopper(mode)
|
src = hopper(mode)
|
||||||
data = ImageQt.toqimage(src)
|
data = ImageQt.toqimage(src)
|
||||||
|
|
||||||
assert isinstance(data, QImage)
|
assert isinstance(data, QImage)
|
||||||
assert not data.isNull()
|
assert not data.isNull()
|
||||||
|
|
||||||
# reload directly from the qimage
|
# reload directly from the qimage
|
||||||
rt = ImageQt.fromqimage(data)
|
rt = ImageQt.fromqimage(data)
|
||||||
if mode in ("L", "P", "1"):
|
if mode in ("L", "P", "1"):
|
||||||
assert_image_equal(rt, src.convert("RGB"))
|
assert_image_equal(rt, src.convert("RGB"))
|
||||||
else:
|
else:
|
||||||
assert_image_equal(rt, src)
|
assert_image_equal(rt, src)
|
||||||
|
|
||||||
if mode == "1":
|
if mode == "1":
|
||||||
# BW appears to not save correctly on QT4 and QT5
|
# BW appears to not save correctly on QT5
|
||||||
# kicks out errors on console:
|
# kicks out errors on console:
|
||||||
# libpng warning: Invalid color type/bit depth combination
|
# libpng warning: Invalid color type/bit depth combination
|
||||||
# in IHDR
|
# in IHDR
|
||||||
# libpng error: Invalid IHDR data
|
# libpng error: Invalid IHDR data
|
||||||
continue
|
return
|
||||||
|
|
||||||
# Test saving the file
|
# Test saving the file
|
||||||
tempfile = str(tmp_path / f"temp_{mode}.png")
|
tempfile = str(tmp_path / f"temp_{mode}.png")
|
||||||
data.save(tempfile)
|
data.save(tempfile)
|
||||||
|
|
||||||
# Check that it actually worked.
|
# Check that it actually worked.
|
||||||
assert_image_equal_tofile(src, tempfile)
|
assert_image_equal_tofile(src, tempfile)
|
||||||
|
|