Merge branch 'main' into codecov

This commit is contained in:
Andrew Murray 2022-08-27 15:51:16 +10:00
commit c2007e7558
165 changed files with 3358 additions and 1714 deletions

View File

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

View File

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

View File

@ -13,13 +13,17 @@ aptget_update()
return 1 return 1
fi fi
} }
if [[ $(uname) != CYGWIN* ]]; then
aptget_update || aptget_update retry || aptget_update retry aptget_update || aptget_update retry || aptget_update retry
fi
set -e set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-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,13 +35,13 @@ 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 [[ $(uname) != CYGWIN* ]]; then
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi python3 -m pip install numpy
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0 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 python3 -m pip install pyqt6
fi fi
@ -52,3 +56,6 @@ pushd depends && ./install_raqm.sh && popd
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd
else
cd depends && ./install_extra_test_images.sh && cd ..
fi

View File

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

1
.github/mergify.yml vendored
View File

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

View File

@ -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:

View File

@ -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

View File

@ -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
View 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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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)
------------------ ------------------

View File

@ -85,6 +85,8 @@ release-test:
sdist: sdist:
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
python3 -m build --sdist python3 -m build --sdist
python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
python3 -m twine check --strict dist/*
.PHONY: test .PHONY: test
test: test:

View File

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

View File

@ -24,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):

View File

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

Binary file not shown.

BIN
Tests/images/ati1.dds Normal file

Binary file not shown.

BIN
Tests/images/ati1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

BIN
Tests/images/ati2.dds Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 B

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

BIN
Tests/images/issue_6194.j2k Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
Tests/images/rgba.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
Tests/images/tiny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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")

View File

@ -51,7 +51,6 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE): with Image.open(TEST_FILE):
pass pass
@pytest.mark.xfail(reason="different exception")
def test_exception_ico(self): def test_exception_ico(self):
with pytest.raises(Image.DecompressionBombError): with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.ico"): with Image.open("Tests/images/decompression_bomb.ico"):
@ -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"):

View File

@ -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,10 +335,11 @@ 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: )
def test_apng_sequence_errors(test_file):
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{f}") as im: with Image.open(f"Tests/images/apng/{test_file}") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()
@ -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_",

View File

@ -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:

View File

@ -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,9 +61,9 @@ 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)
@ -75,9 +77,9 @@ def test_read_n0():
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)
@ -91,9 +93,9 @@ def test_read_n():
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)
@ -107,9 +109,9 @@ def test_read_eof():
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)
@ -122,9 +124,9 @@ def test_readline():
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",

View File

@ -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"),
( (

View File

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

View File

@ -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"

View File

@ -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,8 +365,15 @@ 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_GIF, 1),
("Tests/images/comment_after_last_frame.gif", 2),
("Tests/images/iss634.gif", 42),
),
)
def test_n_frames(path, n_frames):
# Test is_animated before n_frames # Test is_animated before n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
@ -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):

View File

@ -78,16 +78,13 @@ 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):
out = str(tmp_path / "temp.im") out = str(tmp_path / "temp.im")

View File

@ -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):

View File

@ -135,9 +135,9 @@ 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)
@ -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")
if not argument:
TiffImagePlugin.STRIP_SIZE = 2**18 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

View File

@ -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,8 +27,8 @@ 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"
@ -48,27 +52,34 @@ 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"
@ -116,8 +127,17 @@ 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:
exif = im.getexif()
del exif[296]
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: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
@ -141,8 +161,8 @@ 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
@ -160,8 +180,8 @@ def test_mp_attribute():
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
@ -204,8 +224,8 @@ def test_eoferror():
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_image_grab(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_image_grab(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
im0 = im.tobytes() im0 = im.tobytes()
@ -219,14 +239,39 @@ def test_image_grab():
assert im0 != im1 assert im0 != im1
def test_save(): @pytest.mark.parametrize("test_file", test_files)
# Note that only individual frames can be saved at present def test_save(test_file):
for test_file in test_files:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
jpg0 = frame_roundtrip(im) jpg0 = roundtrip(im)
assert_image_similar(im, jpg0, 30) assert_image_similar(im, jpg0, 30)
im.seek(1) im.seek(1)
assert im.tell() == 1 assert im.tell() == 1
jpg1 = frame_roundtrip(im) jpg1 = roundtrip(im)
assert_image_similar(im, jpg1, 30) assert_image_similar(im, jpg1, 30)
def test_save_all():
for test_file in test_files:
with Image.open(test_file) as im:
im_reloaded = roundtrip(im, save_all=True)
im.seek(0)
assert_image_similar(im, im_reloaded, 30)
im.seek(1)
im_reloaded.seek(1)
assert_image_similar(im, im_reloaded, 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

View File

@ -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")

View File

@ -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):

View File

@ -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:

View File

@ -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")

View File

@ -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

View File

@ -18,18 +18,15 @@ _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): def roundtrip(original_im):
out = str(tmp_path / "temp.tga") out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle) original_im.save(out, rle=rle)
with Image.open(out) as saved_im: with Image.open(out) as saved_im:
if rle: if rle:
assert ( assert saved_im.info["compression"] == original_im.info["compression"]
saved_im.info["compression"] == original_im.info["compression"]
)
assert saved_im.info["orientation"] == original_im.info["orientation"] assert saved_im.info["orientation"] == original_im.info["orientation"]
if mode == "P": if mode == "P":
assert saved_im.getpalette() == original_im.getpalette() assert saved_im.getpalette() == original_im.getpalette()
@ -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"

View File

@ -70,6 +70,15 @@ class TestFileTiff:
im.load() im.load()
im.close() im.close()
def test_seek_after_close(self):
im = Image.open("Tests/images/multipage.tiff")
im.close()
with pytest.raises(ValueError):
im.n_frames
with pytest.raises(ValueError):
im.seek(1)
def test_context_manager(self): def test_context_manager(self):
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
@ -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")

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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,19 +45,15 @@ class TestImage:
"YCbCr", "YCbCr",
"LAB", "LAB",
"HSV", "HSV",
]: ),
)
def test_image_modes_success(self, mode):
Image.new(mode, (1, 1)) 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",
"BGR;15",
"BGR;16",
"BGR;24",
"BGR;32",
]:
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
Image.new(mode, (1, 1)) Image.new(mode, (1, 1))
assert str(e.value) == "unrecognized image mode" assert str(e.value) == "unrecognized image mode"
@ -539,11 +536,10 @@ 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)
@ -565,11 +561,10 @@ 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)
@ -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

View File

@ -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,20 +201,22 @@ class TestImageGetPixel(AccessTest):
"RGBX", "RGBX",
"CMYK", "CMYK",
"YCbCr", "YCbCr",
): ),
)
def test_basic(self, mode):
self.check(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)

View File

@ -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,6 +35,7 @@ def test_toarray():
test_with_dtype(numpy.float64) test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8) test_with_dtype(numpy.uint8)
if parse_version(numpy.__version__) >= parse_version("1.23"):
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
with pytest.raises(OSError): with pytest.raises(OSError):
numpy.array(im_truncated) numpy.array(im_truncated)
@ -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

View File

@ -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,8 +268,8 @@ 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)
@ -282,9 +296,6 @@ def test_matrix_xyz():
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():
# Arrange # Arrange

View File

@ -1,14 +1,17 @@
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 # Internal copy method
im = hopper(mode) im = hopper(mode)
out = im.copy() out = im.copy()

View File

@ -5,8 +5,8 @@ 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)
@ -14,9 +14,6 @@ def test_crop():
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():
def crop(*bbox): def crop(*bbox):

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,8 +103,8 @@ 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)
@ -111,8 +113,8 @@ class TestImagingPaste:
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)
@ -133,8 +135,8 @@ class TestImagingPaste:
], ],
) )
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)
@ -155,8 +157,8 @@ class TestImagingPaste:
], ],
) )
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)
@ -177,8 +179,8 @@ class TestImagingPaste:
], ],
) )
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)
@ -199,8 +201,8 @@ class TestImagingPaste:
], ],
) )
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)
@ -221,8 +223,8 @@ class TestImagingPaste:
], ],
) )
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)
@ -234,8 +236,8 @@ class TestImagingPaste:
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)]
@ -256,8 +258,8 @@ class TestImagingPaste:
], ],
) )
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"
@ -278,8 +280,8 @@ class TestImagingPaste:
], ],
) )
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"
@ -300,8 +302,8 @@ class TestImagingPaste:
], ],
) )
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"

View File

@ -1,5 +1,7 @@
import pytest import pytest
from PIL import Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
@ -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 + 1)
with pytest.raises(TypeError):
im.point(lambda x: x - 1) im.point(lambda x: x - 1)
with pytest.raises(TypeError): 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 / 1)
im.point(lambda x: x + x)
with pytest.raises(TypeError):
im.point(lambda x: x * x)
with pytest.raises(TypeError):
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

View File

@ -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:

View File

@ -100,8 +100,8 @@ 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
@ -111,8 +111,8 @@ class TestImagingCoreResampleAccuracy:
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
@ -122,8 +122,8 @@ class TestImagingCoreResampleAccuracy:
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
@ -133,7 +133,8 @@ class TestImagingCoreResampleAccuracy:
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,8 +146,8 @@ 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
@ -158,8 +159,8 @@ class TestImagingCoreResampleAccuracy:
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
@ -169,8 +170,8 @@ class TestImagingCoreResampleAccuracy:
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
@ -180,8 +181,8 @@ class TestImagingCoreResampleAccuracy:
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
@ -191,8 +192,8 @@ class TestImagingCoreResampleAccuracy:
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
@ -204,8 +205,8 @@ class TestImagingCoreResampleAccuracy:
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 = (
@ -419,16 +420,19 @@ 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,
): ),
)
def test_wrong_arguments(self, resample):
im = hopper()
im.resize((32, 32), resample, (0, 0, im.width, im.height)) 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, im.width, im.height))
im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 20, 100))
@ -509,9 +513,11 @@ 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)
)
def test_formats(self, mode, resample):
im = hopper(mode) im = hopper(mode)
box = (20, 20, im.size[0] - 20, im.size[1] - 20) box = (20, 20, im.size[0] - 20, im.size[1] - 20)
with_box = im.resize((32, 32), resample, box) with_box = im.resize((32, 32), resample, box)

View File

@ -22,19 +22,10 @@ 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",
"I",
"F",
"RGB",
"RGBA",
"CMYK",
"YCbCr",
"I;16",
]: # exotic mode
im = hopper(mode) im = hopper(mode)
r = self.resize(im, (15, 12), Image.Resampling.NEAREST) r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
assert r.mode == mode assert r.mode == mode
@ -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) )
def test_reduce_filters(self, resample):
r = self.resize(hopper("RGB"), (15, 12), resample)
assert r.mode == "RGB" assert r.mode == "RGB"
assert r.size == (15, 12) 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) )
def test_enlarge_filters(self, resample):
r = self.resize(hopper("RGB"), (212, 195), resample)
assert r.mode == "RGB" assert r.mode == "RGB"
assert r.size == (212, 195) 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,44 +111,34 @@ class TestImagingCoreResize:
} }
samples["dirty"].putpixel((1, 1), 128) samples["dirty"].putpixel((1, 1), 128)
for f in [
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
]:
# samples resized with current filter # samples resized with current filter
references = { references = {
name: self.resize(ch, (4, 4), f) for name, ch in samples.items() name: self.resize(ch, (4, 4), resample) for name, ch in samples.items()
} }
for mode, channels_set in [
("RGB", ("blank", "filled", "dirty")),
("RGBA", ("blank", "blank", "filled", "dirty")),
("LA", ("filled", "dirty")),
]:
for channels in set(permutations(channels_set)): for channels in set(permutations(channels_set)):
# compile image from different channels permutations # compile image from different channels permutations
im = Image.merge(mode, [samples[ch] for ch in channels]) im = Image.merge(mode, [samples[ch] for ch in channels])
resized = self.resize(im, (4, 4), f) resized = self.resize(im, (4, 4), resample)
for i, ch in enumerate(resized.split()): for i, ch in enumerate(resized.split()):
# check what resized channel in image is the same # check what resized channel in image is the same
# as separately resized channel # as separately resized channel
assert_image_equal(ch, references[channels[i]]) assert_image_equal(ch, references[channels[i]])
def test_enlarge_zero(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(Image.new("RGB", (0, 0), "white"), (212, 195), f) )
def test_enlarge_zero(self, resample):
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
assert r.mode == "RGB" assert r.mode == "RGB"
assert r.size == (212, 195) assert r.size == (212, 195)
assert r.getdata()[0] == (0, 0, 0) assert r.getdata()[0] == (0, 0, 0)
@ -179,12 +185,11 @@ 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
@ -195,12 +200,11 @@ class TestReducingGapResize:
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_2(self, gradients_image): @pytest.mark.parametrize(
for box, epsilon in [ "box, epsilon",
(None, 1.5), ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)),
((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) 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=2.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
@ -211,12 +215,11 @@ class TestReducingGapResize:
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_3(self, gradients_image): @pytest.mark.parametrize(
for box, epsilon in [ "box, epsilon",
(None, 1), ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)),
((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) 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=3.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
@ -227,8 +230,8 @@ class TestReducingGapResize:
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_8(self, gradients_image): @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)))
for box in [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) 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=8.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
@ -236,11 +239,11 @@ class TestReducingGapResize:
assert_image_equal(ref, im) assert_image_equal(ref, im)
def test_box_filter(self, gradients_image): @pytest.mark.parametrize(
for box, epsilon in [ "box, epsilon",
((0, 0, 512, 512), 5.5), (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.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) ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
@ -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))

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image from PIL import Image
from .helper import ( from .helper import (
@ -22,14 +24,14 @@ 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) rotate(im, im.mode, angle)
@ -37,8 +39,8 @@ def test_angle():
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
def test_zero(): @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
for angle in (0, 45, 90, 180, 270): def test_zero(angle):
im = Image.new("RGB", (0, 0)) im = Image.new("RGB", (0, 0))
rotate(im, im.mode, angle) rotate(im, im.mode, angle)

View File

@ -1,3 +1,5 @@
import pytest
from PIL.Image import Transpose from PIL.Image import Transpose
from . import helper from . import helper
@ -9,8 +11,8 @@ 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
@ -22,12 +24,9 @@ def test_flip_left_right():
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)
@pytest.mark.parametrize("mode", HOPPER)
def test_flip_top_bottom(): def test_flip_top_bottom(mode):
def transpose(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
@ -39,12 +38,9 @@ def test_flip_top_bottom():
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)
@pytest.mark.parametrize("mode", HOPPER)
def test_rotate_90(): def test_rotate_90(mode):
def transpose(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
@ -56,12 +52,9 @@ def test_rotate_90():
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)
@pytest.mark.parametrize("mode", HOPPER)
def test_rotate_180(): def test_rotate_180(mode):
def transpose(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
@ -73,12 +66,9 @@ def test_rotate_180():
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)
@pytest.mark.parametrize("mode", HOPPER)
def test_rotate_270(): def test_rotate_270(mode):
def transpose(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
@ -90,12 +80,9 @@ def test_rotate_270():
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)
@pytest.mark.parametrize("mode", HOPPER)
def test_transpose(): def test_transpose(mode):
def 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
@ -107,12 +94,9 @@ def test_transpose():
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)
@pytest.mark.parametrize("mode", HOPPER)
def test_tranverse(): def test_tranverse(mode):
def transpose(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
@ -124,12 +108,9 @@ def test_tranverse():
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)
@pytest.mark.parametrize("mode", HOPPER)
def test_roundtrip(): def test_roundtrip(mode):
for mode in HOPPER:
im = HOPPER[mode] im = HOPPER[mode]
def transpose(first, second): def transpose(first, second):

View File

@ -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()

View File

@ -625,10 +625,10 @@ 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)
@ -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)

View File

@ -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
with pytest.warns(DeprecationWarning) as log:
size = draw.textsize("ImageDraw2", font) 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")

View File

@ -65,9 +65,12 @@ class TestImageFont:
return font_bytes return font_bytes
def test_font_with_filelike(self): def test_font_with_filelike(self):
ImageFont.truetype( ttf = ImageFont.truetype(
self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
) )
ttf_copy = ttf.font_variant()
assert ttf_copy.font_bytes == ttf.font_bytes
self._render(self._font_as_bytes()) self._render(self._font_as_bytes())
# Usage note: making two fonts from the same buffer fails. # Usage note: making two fonts from the same buffer fails.
# shared_bytes = self._font_as_bytes() # shared_bytes = self._font_as_bytes()
@ -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,6 +245,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)
with pytest.warns(DeprecationWarning) as log:
# Test that textsize() correctly connects to multiline_textsize() # Test that textsize() correctly connects to multiline_textsize()
assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize(
TEST_TEXT, font=ttf TEST_TEXT, font=ttf
@ -255,16 +259,41 @@ class TestImageFont:
# to multiline_textsize() # to multiline_textsize()
draw.textsize(TEST_TEXT, font=ttf, spacing=4) draw.textsize(TEST_TEXT, font=ttf, spacing=4)
draw.textsize(TEST_TEXT, ttf, 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_textbbox corresponds to ImageFont.textbbox()
# for single line text
assert ttf.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=ttf)
# Test that textbbox() can pass on additional arguments
# to multiline_textbbox()
draw.textbbox((0, 0), TEST_TEXT, font=ttf, spacing=4)
def test_multiline_width(self): def test_multiline_width(self):
ttf = self.get_font() ttf = self.get_font()
im = Image.new(mode="RGB", size=(300, 100)) im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
assert (
draw.textbbox((0, 0), "longest line", font=ttf)[2]
== draw.multiline_textbbox((0, 0), "longest line\nline", font=ttf)[2]
)
with pytest.warns(DeprecationWarning) as log:
assert ( assert (
draw.textsize("longest line", font=ttf)[0] draw.textsize("longest line", font=ttf)[0]
== draw.multiline_textsize("longest line\nline", 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
with pytest.warns(DeprecationWarning) as log:
box_size_a = draw.textsize(word) 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
with pytest.warns(DeprecationWarning) as log:
box_size_b = draw.textsize(word) 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
with pytest.warns(DeprecationWarning) as log:
box_size_a = draw.textsize(word) 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
with pytest.warns(DeprecationWarning) as log:
box_size_b = draw.textsize(word) 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
with pytest.warns(DeprecationWarning) as log:
offset = font.getoffset(text) 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,6 +624,17 @@ 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.getbbox("A") == (0, 4, 12, 16)
assert t.getbbox("AB") == (0, 4, 24, 16)
assert t.getbbox("M") == (0, 4, 12, 16)
assert t.getbbox("y") == (0, 7, 12, 20)
assert t.getbbox("a") == (0, 7, 12, 16)
assert t.getlength("A") == 12
assert t.getlength("AB") == 24
assert t.getlength("M") == 12
assert t.getlength("y") == 12
assert t.getlength("a") == 12
with pytest.warns(DeprecationWarning) as log:
assert t.getsize("A") == (12, 16) assert t.getsize("A") == (12, 16)
assert t.getsize("AB") == (24, 16) assert t.getsize("AB") == (24, 16)
assert t.getsize("M") == (12, 16) assert t.getsize("M") == (12, 16)
@ -571,6 +646,7 @@ class TestImageFont:
assert t.getsize_multiline("ABC\n") == (36, 36) assert t.getsize_multiline("ABC\n") == (36, 36)
assert t.getsize_multiline("ABC\nA") == (36, 36) assert t.getsize_multiline("ABC\nA") == (36, 36)
assert t.getsize_multiline("ABC\nAaaa") == (48, 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,6 +654,13 @@ class TestImageFont:
# Act / Assert # Act / Assert
for stroke_width in [0, 2]: for stroke_width in [0, 2]:
assert t.getbbox("A", stroke_width=stroke_width) == (
0 - stroke_width,
4 - stroke_width,
12 + stroke_width,
16 + stroke_width,
)
with pytest.warns(DeprecationWarning) as log:
assert t.getsize("A", stroke_width=stroke_width) == ( assert t.getsize("A", stroke_width=stroke_width) == (
12 + stroke_width * 2, 12 + stroke_width * 2,
16 + stroke_width * 2, 16 + stroke_width * 2,
@ -586,6 +669,7 @@ class TestImageFont:
48 + stroke_width * 2, 48 + stroke_width * 2,
36 + stroke_width * 4, 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)

View File

@ -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():

View File

@ -345,12 +345,16 @@ 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"):
with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im:
assert im.getexif()[0x0112] == 3 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
# to have a different orientation # to have a different orientation

View File

@ -16,8 +16,8 @@ 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)
@ -32,12 +32,12 @@ def test_sanity(tmp_path):
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")

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