diff --git a/.appveyor.yml b/.appveyor.yml
index 1cca224ab..20908052b 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -25,8 +25,8 @@ install:
- mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
-- ..\pillow-depends\gs9561w32.exe /S
-- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
+- ..\pillow-depends\gs1000w32.exe /S
+- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
diff --git a/.ci/install.sh b/.ci/install.sh
index c588af42f..518b66acc 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -35,15 +35,13 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-python3 -m pip install test-image-results
if [[ $(uname) != CYGWIN* ]]; then
- # TODO Remove condition when NumPy supports 3.11
- if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
+ python3 -m pip install numpy
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
- sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0
+ sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6
fi
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 000000000..d1d824335
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:base"
+ ],
+ "labels": [
+ "Dependency"
+ ],
+ "packageRules": [
+ {
+ "groupName": "github-actions",
+ "matchManagers": ["github-actions"],
+ "separateMajorMinor": "false"
+ }
+ ],
+ "schedule": ["on the 3rd day of the month"]
+}
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
index 0e0abaf95..db0307046 100644
--- a/.github/workflows/cifuzz.yml
+++ b/.github/workflows/cifuzz.yml
@@ -11,6 +11,13 @@ on:
- "**.h"
workflow_dispatch:
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
Fuzzing:
runs-on: ubuntu-latest
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 4540fb5af..6195f973b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,6 +2,13 @@ name: Lint
on: [push, pull_request, workflow_dispatch]
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
@@ -13,7 +20,7 @@ jobs:
- uses: actions/checkout@v3
- name: pre-commit cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
@@ -21,7 +28,7 @@ jobs:
lint-pre-commit-
- name: Set up Python
- uses: actions/setup-python@v3
+ uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: pip
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index 06b829645..65f2b81d5 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -12,11 +12,9 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-python3 -m pip install test-image-results
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
-# TODO Remove condition when NumPy supports 3.11
-if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
+python3 -m pip install numpy
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
index ad66117b1..9e2fdc096 100644
--- a/.github/workflows/release-drafter.yml
+++ b/.github/workflows/release-drafter.yml
@@ -7,8 +7,18 @@ on:
- main
workflow_dispatch:
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
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'
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index cc5e0d488..ffac91cec 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -8,6 +8,10 @@ on:
permissions:
issues: write
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
stale:
if: github.repository_owner == 'python-pillow'
@@ -16,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
- uses: actions/stale@v5
+ uses: actions/stale@v6
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 2e8fc9c09..5b9ab0eda 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -2,6 +2,13 @@ name: Test Cygwin
on: [push, pull_request, workflow_dispatch]
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
runs-on: windows-latest
@@ -41,7 +48,7 @@ jobs:
qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH
- uses: egor-tensin/cleanup-path@v1
+ uses: egor-tensin/cleanup-path@v2
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
@@ -99,6 +106,8 @@ jobs:
name: Cygwin Python 3.${{ matrix.python-minor-version }}
success:
+ permissions:
+ contents: none
needs: build
runs-on: ubuntu-latest
name: Cygwin Test Successful
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 2b4dc6b52..c68d43935 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -2,6 +2,13 @@ name: Test Docker
on: [push, pull_request, workflow_dispatch]
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
@@ -76,12 +83,14 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v3
with:
flags: GHA_Docker
name: ${{ matrix.docker }}
success:
+ permissions:
+ contents: none
needs: build
runs-on: ubuntu-latest
name: Docker Test Successful
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 7b5cc8a97..ccf6e193a 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -2,6 +2,13 @@ name: Test MinGW
on: [push, pull_request, workflow_dispatch]
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
runs-on: windows-latest
@@ -70,13 +77,15 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage
- run: |
- python3 -m pip install codecov
- bash <(curl -s https://codecov.io/bash) -F GHA_Windows
- env:
- CODECOV_NAME: ${{ matrix.name }}
+ uses: codecov/codecov-action@v3
+ with:
+ file: ./coverage.xml
+ flags: GHA_Windows
+ name: ${{ matrix.name }}
success:
+ permissions:
+ contents: none
needs: build
runs-on: ubuntu-latest
name: MinGW Test Successful
diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml
index 21a2b469e..219189cf2 100644
--- a/.github/workflows/test-valgrind.yml
+++ b/.github/workflows/test-valgrind.yml
@@ -13,6 +13,13 @@ on:
- "**.h"
workflow_dispatch:
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
@@ -21,7 +28,7 @@ jobs:
fail-fast: false
matrix:
docker: [
- ubuntu-20.04-focal-amd64-valgrind,
+ ubuntu-22.04-jammy-amd64-valgrind,
]
dockerTag: [main]
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 358461b38..36bd03e7e 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -2,6 +2,13 @@ name: Test Windows
on: [push, pull_request, workflow_dispatch]
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
runs-on: windows-latest
@@ -33,7 +40,7 @@ jobs:
# sets env: pythonLocation
- name: Set up Python
- uses: actions/setup-python@v3
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
@@ -52,8 +59,8 @@ jobs:
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
- winbuild\depends\gs9561w32.exe /S
- echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
+ winbuild\depends\gs1000w32.exe /S
+ echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
@@ -63,7 +70,7 @@ jobs:
- name: Cache build
id: build-cache
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: winbuild\build
key:
@@ -168,7 +175,7 @@ jobs:
shell: pwsh
- name: Upload coverage
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: GHA_Windows
@@ -189,6 +196,8 @@ jobs:
path: dist\*.whl
success:
+ permissions:
+ contents: none
needs: build
runs-on: ubuntu-latest
name: Windows Test Successful
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d41f4b571..4c8a1b85f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -2,6 +2,13 @@ name: Test
on: [push, pull_request, workflow_dispatch]
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
@@ -27,11 +34,6 @@ jobs:
REVERSE: "--reverse"
- python-version: "3.8"
PYTHONOPTIMIZE: 2
- # Include new variables for Codecov
- - os: ubuntu-latest
- codecov-flag: GHA_Ubuntu
- - os: macos-latest
- codecov-flag: GHA_macOS
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
@@ -40,7 +42,7 @@ jobs:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v3
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: pip
@@ -96,7 +98,6 @@ jobs:
- name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: |
- python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
make doccheck
- name: After success
@@ -104,11 +105,15 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
- run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }}
- env:
- CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }}
+ uses: codecov/codecov-action@v3
+ with:
+ file: ./coverage.xml
+ flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }}
+ name: ${{ matrix.os }} Python ${{ matrix.python-version }}
success:
+ permissions:
+ contents: none
needs: build
runs-on: ubuntu-latest
name: Test Successful
diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml
index 9a3192f9d..69f9e5476 100644
--- a/.github/workflows/tidelift.yml
+++ b/.github/workflows/tidelift.yml
@@ -1,4 +1,5 @@
name: Tidelift Align
+
on:
schedule:
- cron: "30 2 * * *" # daily at 02:30 UTC
@@ -12,6 +13,13 @@ on:
- ".github/workflows/tidelift.yml"
workflow_dispatch:
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build:
if: github.repository_owner == 'python-pillow'
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1806db54c..f81bcb956 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
- rev: 22.3.0
+ rev: 22.8.0
hooks:
- id: black
args: ["--target-version", "py37"]
@@ -14,18 +14,18 @@ repos:
- id: isort
- repo: https://github.com/asottile/yesqa
- rev: v1.3.0
+ rev: v1.4.0
hooks:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
- rev: v1.2.0
+ rev: v1.3.1
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8
- rev: 4.0.1
+ rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
@@ -37,13 +37,14 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.2.0
+ rev: v4.3.0
hooks:
- id: check-merge-conflict
+ - id: check-json
- id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v0.6
+ rev: v0.6.1
hooks:
- id: sphinx-lint
diff --git a/CHANGES.rst b/CHANGES.rst
index 658a950dd..c3e60acff 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,135 @@
Changelog (Pillow)
==================
-9.2.0 (unreleased)
+9.3.0 (unreleased)
------------------
+- Added reading of TIFF child images #6569
+ [radarhere]
+
+- Improved ImageOps palette handling #6596
+ [PososikTeam, radarhere]
+
+- Defer parsing of palette into colors #6567
+ [radarhere]
+
+- Apply transparency to P images in ImageTk.PhotoImage #6559
+ [radarhere]
+
+- Use rounding in ImageOps contain() and pad() #6522
+ [bibinhashley, radarhere]
+
+- Fixed GIF remapping to palette with duplicate entries #6548
+ [radarhere]
+
+- Allow remap_palette() to return an image with less than 256 palette entries #6543
+ [radarhere]
+
+- Corrected BMP and TGA palette size when saving #6500
+ [radarhere]
+
+- Do not call load() before draft() in Image.thumbnail #6539
+ [radarhere]
+
+- Copy palette when converting from P to PA #6497
+ [radarhere]
+
+- Allow RGB and RGBA values for PA image putpixel #6504
+ [radarhere]
+
+- Removed support for tkinter in PyPy before Python 3.6 #6551
+ [nulano]
+
+- Do not use CCITTFaxDecode filter if libtiff is not available #6518
+ [radarhere]
+
+- Fallback to not using mmap if buffer is not large enough #6510
+ [radarhere]
+
+- Fixed writing bytes as ASCII tag #6493
+ [radarhere]
+
+- Open 1 bit EPS in mode 1 #6499
+ [radarhere]
+
+- Removed support for tkinter before Python 1.5.2 #6549
+ [radarhere]
+
+- 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]
diff --git a/MANIFEST.in b/MANIFEST.in
index 26f9401f2..08f6dfc08 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -25,6 +25,7 @@ exclude .coveragerc
exclude .editorconfig
exclude .readthedocs.yml
exclude codecov.yml
+exclude renovate.json
global-exclude .git*
global-exclude *.pyc
global-exclude *.so
diff --git a/Makefile b/Makefile
index 219dda1de..8f2862948 100644
--- a/Makefile
+++ b/Makefile
@@ -17,11 +17,12 @@ coverage:
.PHONY: doc
doc:
+ python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs html
.PHONY: doccheck
doccheck:
- $(MAKE) -C docs html
+ $(MAKE) doc
# Don't make our tests rely on the links in the docs being up every single build.
# We don't control them. But do check, and update them to the target of their redirects.
$(MAKE) -C docs linkcheck || true
diff --git a/README.md b/README.md
index 5e9adaf7e..e7c0ebc5a 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,9 @@ As of 2019, Pillow development is
+
diff --git a/RELEASING.md b/RELEASING.md
index aa7511c8a..b05067484 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -96,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related
## Binary Distributions
### Windows
-* [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174.
-* [ ] Download and extract tarball from `@cgohlke` and copy into `dist/`
+* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
+ and copy into `dist/`
### Mac and Linux
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
diff --git a/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf b/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf
new file mode 100644
index 000000000..c065f59a9
Binary files /dev/null and b/Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf differ
diff --git a/Tests/images/1.eps b/Tests/images/1.eps
new file mode 100644
index 000000000..727dc9b7f
Binary files /dev/null and b/Tests/images/1.eps differ
diff --git a/Tests/images/ati1.dds b/Tests/images/ati1.dds
new file mode 100644
index 000000000..747e4b1b9
Binary files /dev/null and b/Tests/images/ati1.dds differ
diff --git a/Tests/images/ati1.png b/Tests/images/ati1.png
new file mode 100644
index 000000000..790d7d7db
Binary files /dev/null and b/Tests/images/ati1.png differ
diff --git a/Tests/images/ati2.dds b/Tests/images/ati2.dds
new file mode 100644
index 000000000..3ac5f7956
Binary files /dev/null and b/Tests/images/ati2.dds differ
diff --git a/Tests/images/child_ifd.tiff b/Tests/images/child_ifd.tiff
new file mode 100644
index 000000000..700185d88
Binary files /dev/null and b/Tests/images/child_ifd.tiff differ
diff --git a/Tests/images/child_ifd_jpeg.tiff b/Tests/images/child_ifd_jpeg.tiff
new file mode 100644
index 000000000..f5e3d129d
Binary files /dev/null and b/Tests/images/child_ifd_jpeg.tiff differ
diff --git a/Tests/images/comment_after_only_frame.gif b/Tests/images/comment_after_only_frame.gif
new file mode 100644
index 000000000..8188b6847
Binary files /dev/null and b/Tests/images/comment_after_only_frame.gif differ
diff --git a/Tests/images/decompression_bomb_extents.gif b/Tests/images/decompression_bomb_extents.gif
new file mode 100644
index 000000000..0d5ff03f5
Binary files /dev/null and b/Tests/images/decompression_bomb_extents.gif differ
diff --git a/Tests/images/hopper_rle8_greyscale.bmp b/Tests/images/hopper_rle8_greyscale.bmp
new file mode 100644
index 000000000..ead32ff95
Binary files /dev/null and b/Tests/images/hopper_rle8_greyscale.bmp differ
diff --git a/Tests/images/input_bw_one_band.fpx b/Tests/images/input_bw_one_band.fpx
new file mode 100644
index 000000000..9bdc53763
Binary files /dev/null and b/Tests/images/input_bw_one_band.fpx differ
diff --git a/Tests/images/input_bw_one_band.png b/Tests/images/input_bw_one_band.png
new file mode 100644
index 000000000..6b4c1f376
Binary files /dev/null and b/Tests/images/input_bw_one_band.png differ
diff --git a/Tests/images/mmap_error.bmp b/Tests/images/mmap_error.bmp
new file mode 100644
index 000000000..04df163d7
Binary files /dev/null and b/Tests/images/mmap_error.bmp differ
diff --git a/Tests/images/palette_negative.png b/Tests/images/palette_negative.png
index 938a7285f..7fcfd29a0 100644
Binary files a/Tests/images/palette_negative.png and b/Tests/images/palette_negative.png differ
diff --git a/Tests/images/palette_sepia.png b/Tests/images/palette_sepia.png
index f3fc93253..9e7d6b034 100644
Binary files a/Tests/images/palette_sepia.png and b/Tests/images/palette_sepia.png differ
diff --git a/Tests/images/palette_wedge.png b/Tests/images/palette_wedge.png
index 23fb7940d..4b3d9ff3a 100644
Binary files a/Tests/images/palette_wedge.png and b/Tests/images/palette_wedge.png differ
diff --git a/Tests/images/rectangle_surrounding_text.png b/Tests/images/rectangle_surrounding_text.png
index 2b75a5e9c..ca77cea73 100644
Binary files a/Tests/images/rectangle_surrounding_text.png and b/Tests/images/rectangle_surrounding_text.png differ
diff --git a/Tests/images/rgb32bf-abgr.bmp b/Tests/images/rgb32bf-abgr.bmp
new file mode 100644
index 000000000..2443714ca
Binary files /dev/null and b/Tests/images/rgb32bf-abgr.bmp differ
diff --git a/Tests/images/rgba.psd b/Tests/images/rgba.psd
new file mode 100644
index 000000000..45fb7c3cc
Binary files /dev/null and b/Tests/images/rgba.psd differ
diff --git a/Tests/images/xmp_tags_orientation_exiftool.png b/Tests/images/xmp_tags_orientation_exiftool.png
new file mode 100644
index 000000000..10f0f4400
Binary files /dev/null and b/Tests/images/xmp_tags_orientation_exiftool.png differ
diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py
index 5786764a6..10a172b46 100644
--- a/Tests/oss-fuzz/fuzzers.py
+++ b/Tests/oss-fuzz/fuzzers.py
@@ -33,9 +33,9 @@ def fuzz_font(data):
# different font objects.
return
- font.getsize_multiline("ABC\nAaaa")
+ font.getbbox("ABC")
font.getmask("test text")
with Image.new(mode="RGBA", size=(200, 200)) as 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")
diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py
index 59fbac527..3fd982474 100644
--- a/Tests/test_000_sanity.py
+++ b/Tests/test_000_sanity.py
@@ -1,19 +1,18 @@
-import PIL
-import PIL.Image
+from PIL import Image
def test_sanity():
# Make sure we have the binary extension
- PIL.Image.core.new("L", (100, 100))
+ Image.core.new("L", (100, 100))
# Create an image and do stuff with it.
- im = PIL.Image.new("1", (100, 100))
+ im = Image.new("1", (100, 100))
assert (im.mode, im.size) == ("1", (100, 100))
assert len(im.tobytes()) == 1300
# Create images in all remaining major modes.
- PIL.Image.new("L", (100, 100))
- PIL.Image.new("P", (100, 100))
- PIL.Image.new("RGB", (100, 100))
- PIL.Image.new("I", (100, 100))
- PIL.Image.new("F", (100, 100))
+ Image.new("L", (100, 100))
+ Image.new("P", (100, 100))
+ Image.new("RGB", (100, 100))
+ Image.new("I", (100, 100))
+ Image.new("F", (100, 100))
diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py
index d85d1f3c2..63071b78c 100644
--- a/Tests/test_decompression_bomb.py
+++ b/Tests/test_decompression_bomb.py
@@ -61,6 +61,11 @@ class TestDecompressionBomb:
with Image.open("Tests/images/decompression_bomb.gif"):
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):
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
diff --git a/Tests/test_features.py b/Tests/test_features.py
index 284f72205..c4e9cd368 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -70,14 +70,14 @@ def test_libimagequant_version():
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
-def test_check_modules():
- for feature in features.modules:
- assert features.check_module(feature) in [True, False]
+@pytest.mark.parametrize("feature", features.modules)
+def test_check_modules(feature):
+ assert features.check_module(feature) in [True, False]
-def test_check_codecs():
- for feature in features.codecs:
- assert features.check_codec(feature) in [True, False]
+@pytest.mark.parametrize("feature", features.codecs)
+def test_check_codecs(feature):
+ assert features.check_codec(feature) in [True, False]
def test_check_warns_on_nonexistent():
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index ad61a07cc..cdaad5940 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -39,13 +39,12 @@ def test_apng_basic():
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_fdat():
- with Image.open("Tests/images/apng/split_fdat.png") as im:
- im.seek(im.n_frames - 1)
- assert im.getpixel((0, 0)) == (0, 255, 0, 255)
- assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-
- with Image.open("Tests/images/apng/split_fdat_zero_chunk.png") as im:
+@pytest.mark.parametrize(
+ "filename",
+ ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
+)
+def test_apng_fdat(filename):
+ with Image.open(filename) as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@@ -325,8 +324,9 @@ def test_apng_syntax_errors():
pytest.warns(UserWarning, open)
-def test_apng_sequence_errors():
- test_files = [
+@pytest.mark.parametrize(
+ "test_file",
+ (
"sequence_start.png",
"sequence_gap.png",
"sequence_repeat.png",
@@ -334,12 +334,13 @@ def test_apng_sequence_errors():
"sequence_reorder.png",
"sequence_reorder_chunk.png",
"sequence_fdat_fctl.png",
- ]
- for f in test_files:
- with pytest.raises(SyntaxError):
- with Image.open(f"Tests/images/apng/{f}") as im:
- im.seek(im.n_frames - 1)
- im.load()
+ ),
+)
+def test_apng_sequence_errors(test_file):
+ with pytest.raises(SyntaxError):
+ with Image.open(f"Tests/images/apng/{test_file}") as im:
+ im.seek(im.n_frames - 1)
+ im.load()
def test_apng_save(tmp_path):
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index f214fd6bd..f6860a9a4 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -39,6 +39,13 @@ def test_invalid_file():
BmpImagePlugin.BmpImageFile(fp)
+def test_fallback_if_mmap_errors():
+ # This image has been truncated,
+ # so that the buffer is not large enough when using mmap
+ with Image.open("Tests/images/mmap_error.bmp") as im:
+ assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
+
+
def test_save_to_bytes():
output = io.BytesIO()
im = hopper()
@@ -51,6 +58,18 @@ def test_save_to_bytes():
assert reloaded.format == "BMP"
+def test_small_palette(tmp_path):
+ im = Image.new("P", (1, 1))
+ colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
+ im.putpalette(colors)
+
+ out = str(tmp_path / "temp.bmp")
+ im.save(out)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.getpalette() == colors
+
+
def test_save_too_large(tmp_path):
outfile = str(tmp_path / "temp.bmp")
with Image.new("RGB", (1, 1)) as im:
@@ -129,11 +148,21 @@ def test_rgba_bitfields():
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():
with Image.open("Tests/images/hopper_rle8.bmp") as im:
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
# to have rows with too much data
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py
index b752e217f..65cf6a75e 100644
--- a/Tests/test_file_container.py
+++ b/Tests/test_file_container.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import ContainerIO, Image
from .helper import hopper
@@ -59,89 +61,89 @@ def test_seek_mode_2():
assert container.tell() == 100
-def test_read_n0():
+@pytest.mark.parametrize("bytesmode", (True, False))
+def test_read_n0(bytesmode):
# Arrange
- for bytesmode in (True, False):
- with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
- container = ContainerIO.ContainerIO(fh, 22, 100)
+ with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
+ container = ContainerIO.ContainerIO(fh, 22, 100)
- # Act
- container.seek(81)
- data = container.read()
+ # Act
+ container.seek(81)
+ data = container.read()
- # Assert
- if bytesmode:
- data = data.decode()
- assert data == "7\nThis is line 8\n"
+ # Assert
+ if bytesmode:
+ data = data.decode()
+ assert data == "7\nThis is line 8\n"
-def test_read_n():
+@pytest.mark.parametrize("bytesmode", (True, False))
+def test_read_n(bytesmode):
# Arrange
- for bytesmode in (True, False):
- with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
- container = ContainerIO.ContainerIO(fh, 22, 100)
+ with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
+ container = ContainerIO.ContainerIO(fh, 22, 100)
- # Act
- container.seek(81)
- data = container.read(3)
+ # Act
+ container.seek(81)
+ data = container.read(3)
- # Assert
- if bytesmode:
- data = data.decode()
- assert data == "7\nT"
+ # Assert
+ if bytesmode:
+ data = data.decode()
+ assert data == "7\nT"
-def test_read_eof():
+@pytest.mark.parametrize("bytesmode", (True, False))
+def test_read_eof(bytesmode):
# Arrange
- for bytesmode in (True, False):
- with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
- container = ContainerIO.ContainerIO(fh, 22, 100)
+ with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
+ container = ContainerIO.ContainerIO(fh, 22, 100)
- # Act
- container.seek(100)
- data = container.read()
+ # Act
+ container.seek(100)
+ data = container.read()
- # Assert
- if bytesmode:
- data = data.decode()
- assert data == ""
+ # Assert
+ if bytesmode:
+ data = data.decode()
+ assert data == ""
-def test_readline():
+@pytest.mark.parametrize("bytesmode", (True, False))
+def test_readline(bytesmode):
# Arrange
- for bytesmode in (True, False):
- with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
- container = ContainerIO.ContainerIO(fh, 0, 120)
+ with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
+ container = ContainerIO.ContainerIO(fh, 0, 120)
- # Act
- data = container.readline()
+ # Act
+ data = container.readline()
- # Assert
- if bytesmode:
- data = data.decode()
- assert data == "This is line 1\n"
+ # Assert
+ if bytesmode:
+ data = data.decode()
+ assert data == "This is line 1\n"
-def test_readlines():
+@pytest.mark.parametrize("bytesmode", (True, False))
+def test_readlines(bytesmode):
# Arrange
- for bytesmode in (True, False):
- expected = [
- "This is line 1\n",
- "This is line 2\n",
- "This is line 3\n",
- "This is line 4\n",
- "This is line 5\n",
- "This is line 6\n",
- "This is line 7\n",
- "This is line 8\n",
- ]
- with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
- container = ContainerIO.ContainerIO(fh, 0, 120)
+ expected = [
+ "This is line 1\n",
+ "This is line 2\n",
+ "This is line 3\n",
+ "This is line 4\n",
+ "This is line 5\n",
+ "This is line 6\n",
+ "This is line 7\n",
+ "This is line 8\n",
+ ]
+ with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
+ container = ContainerIO.ContainerIO(fh, 0, 120)
- # Act
- data = container.readlines()
+ # Act
+ data = container.readlines()
- # Assert
- if bytesmode:
- data = [line.decode() for line in data]
- assert data == expected
+ # Assert
+ if bytesmode:
+ data = [line.decode() for line in data]
+ assert data == expected
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index 58447122e..351001199 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -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_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_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_UNORM = "Tests/images/bc5_unorm.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"))
+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(
("image_path", "expected_path"),
(
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 1790f4f77..015dda992 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -124,14 +124,6 @@ def test_file_object(tmp_path):
image1.save(fh, "EPS")
-@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_iobase_object(tmp_path):
- # issue 479
- with Image.open(FILE1) as image1:
- with open(str(tmp_path / "temp_iobase.eps"), "wb") as fh:
- image1.save(fh, "EPS")
-
-
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_bytesio_object():
with open(FILE1, "rb") as f:
@@ -146,6 +138,11 @@ def test_bytesio_object():
assert_image_similar(img, image1_scale1_compare, 5)
+def test_1_mode():
+ with Image.open("Tests/images/1.eps") as im:
+ assert im.mode == "1"
+
+
def test_image_mode_not_supported(tmp_path):
im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps")
@@ -198,25 +195,23 @@ def test_render_scale2():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_resize():
- files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"]
- for fn in files:
- with Image.open(fn) as im:
- new_size = (100, 100)
- im = im.resize(new_size)
- assert im.size == new_size
+@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
+def test_resize(filename):
+ with Image.open(filename) as im:
+ new_size = (100, 100)
+ im = im.resize(new_size)
+ assert im.size == new_size
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_thumbnail():
+@pytest.mark.parametrize("filename", (FILE1, FILE2))
+def test_thumbnail(filename):
# Issue #619
# Arrange
- files = [FILE1, FILE2]
- for fn in files:
- with Image.open(FILE1) as im:
- new_size = (100, 100)
- im.thumbnail(new_size)
- assert max(im.size) == max(new_size)
+ with Image.open(filename) as im:
+ new_size = (100, 100)
+ im.thumbnail(new_size)
+ assert max(im.size) == max(new_size)
def test_read_binary_preview():
@@ -261,20 +256,19 @@ def test_readline(tmp_path):
_test_readline_file_psfile(s, ending)
-def test_open_eps():
- # https://github.com/python-pillow/Pillow/issues/1104
- # Arrange
- FILES = [
+@pytest.mark.parametrize(
+ "filename",
+ (
"Tests/images/illu10_no_preview.eps",
"Tests/images/illu10_preview.eps",
"Tests/images/illuCS6_no_preview.eps",
"Tests/images/illuCS6_preview.eps",
- ]
-
- # Act / Assert
- for filename in FILES:
- with Image.open(filename) as img:
- assert img.mode == "RGB"
+ ),
+)
+def test_open_eps(filename):
+ # https://github.com/python-pillow/Pillow/issues/1104
+ with Image.open(filename) as img:
+ assert img.mode == "RGB"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py
index 818565f88..fa22e90f6 100644
--- a/Tests/test_file_fpx.py
+++ b/Tests/test_file_fpx.py
@@ -2,11 +2,22 @@ import pytest
from PIL import Image
+from .helper import assert_image_equal_tofile
+
FpxImagePlugin = pytest.importorskip(
"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():
# Test an invalid OLE file
invalid_file = "Tests/images/flower.jpg"
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 95fd2553a..bcab43265 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -158,6 +158,9 @@ def test_optimize_correctness():
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
# These do optimize the palette
+ check(256, 511, 256)
+ check(255, 511, 255)
+ check(129, 511, 129)
check(128, 511, 128)
check(64, 511, 64)
check(4, 511, 4)
@@ -167,11 +170,6 @@ def test_optimize_correctness():
check(64, 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():
im = Image.frombytes("L", (16, 16), bytes(range(256)))
@@ -180,6 +178,19 @@ def test_optimize_full_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):
out = str(tmp_path / "temp.gif")
im = hopper()
@@ -388,6 +399,11 @@ def test_no_change():
assert im.is_animated
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():
with Image.open(TEST_GIF) as im:
@@ -777,24 +793,24 @@ def test_identical_frames(tmp_path):
assert reread.info["duration"] == 4500
-def test_identical_frames_to_single_frame(tmp_path):
- for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500):
- out = str(tmp_path / "temp.gif")
- im_list = [
- Image.new("L", (100, 100), "#000"),
- Image.new("L", (100, 100), "#000"),
- Image.new("L", (100, 100), "#000"),
- ]
+@pytest.mark.parametrize(
+ "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)
+)
+def test_identical_frames_to_single_frame(duration, tmp_path):
+ out = str(tmp_path / "temp.gif")
+ im_list = [
+ Image.new("L", (100, 100), "#000"),
+ Image.new("L", (100, 100), "#000"),
+ Image.new("L", (100, 100), "#000"),
+ ]
- im_list[0].save(
- out, save_all=True, append_images=im_list[1:], duration=duration
- )
- with Image.open(out) as reread:
- # Assert that all frames were combined
- assert reread.n_frames == 1
+ im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
+ with Image.open(out) as reread:
+ # Assert that all frames were combined
+ assert reread.n_frames == 1
- # Assert that the new duration is the total of the identical frames
- assert reread.info["duration"] == 8500
+ # Assert that the new duration is the total of the identical frames
+ assert reread.info["duration"] == 8500
def test_number_of_loops(tmp_path):
@@ -982,8 +998,8 @@ def test_append_images(tmp_path):
def test_transparent_optimize(tmp_path):
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
# transparency.
- # Need a palette that isn't using the 0 color, and one that's > 128 items where the
- # transparent color is actually the top palette entry to trigger the bug.
+ # Need a palette that isn't using the 0 color,
+ # where the transparent color is actually the top palette entry to trigger the bug.
data = bytes(range(1, 254))
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
@@ -993,10 +1009,10 @@ def test_transparent_optimize(tmp_path):
im.putpalette(palette)
out = str(tmp_path / "temp.gif")
- im.save(out, transparency=253)
- with Image.open(out) as reloaded:
+ im.save(out, transparency=im.getpixel((252, 0)))
- 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):
@@ -1071,6 +1087,19 @@ def test_palette_save_P(tmp_path):
assert_image_equal(reloaded, im)
+def test_palette_save_duplicate_entries(tmp_path):
+ im = Image.new("P", (1, 2))
+ im.putpixel((0, 1), 1)
+
+ im.putpalette((0, 0, 0, 0, 0, 0))
+
+ out = str(tmp_path / "temp.gif")
+ im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])
+
+ with Image.open(out) as reloaded:
+ assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0)
+
+
def test_palette_save_all_P(tmp_path):
frames = []
colors = ((255, 0, 0), (0, 255, 0))
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index 675210c30..e458a197c 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -78,15 +78,12 @@ def test_eoferror():
im.seek(n_frames - 1)
-def test_roundtrip(tmp_path):
- def roundtrip(mode):
- out = str(tmp_path / "temp.im")
- im = hopper(mode)
- im.save(out)
- assert_image_equal_tofile(im, out)
-
- for mode in ["RGB", "P", "PA"]:
- roundtrip(mode)
+@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
+def test_roundtrip(mode, tmp_path):
+ out = str(tmp_path / "temp.im")
+ im = hopper(mode)
+ im.save(out)
+ assert_image_equal_tofile(im, out)
def test_save_unsupported_mode(tmp_path):
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index c46b27963..fd88f0d37 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -150,27 +150,30 @@ class TestFileJpeg:
assert not im1.info.get("icc_profile")
assert im2.info.get("icc_profile")
- def test_icc_big(self):
+ @pytest.mark.parametrize(
+ "n",
+ (
+ 0,
+ 1,
+ 3,
+ 4,
+ 5,
+ 65533 - 14, # full JPEG marker block
+ 65533 - 14 + 1, # full block plus one byte
+ ImageFile.MAXBLOCK, # full buffer block
+ ImageFile.MAXBLOCK + 1, # full buffer block plus one byte
+ ImageFile.MAXBLOCK * 4 + 3, # large block
+ ),
+ )
+ def test_icc_big(self, n):
# Make sure that the "extra" support handles large blocks
- def test(n):
- # The ICC APP marker can store 65519 bytes per marker, so
- # using a 4-byte test code should allow us to detect out of
- # order issues.
- icc_profile = (b"Test" * int(n / 4 + 1))[:n]
- assert len(icc_profile) == n # sanity
- im1 = self.roundtrip(hopper(), icc_profile=icc_profile)
- assert im1.info.get("icc_profile") == (icc_profile or None)
-
- test(0)
- test(1)
- test(3)
- test(4)
- test(5)
- test(65533 - 14) # full JPEG marker block
- test(65533 - 14 + 1) # full block plus one byte
- test(ImageFile.MAXBLOCK) # full buffer block
- test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte
- test(ImageFile.MAXBLOCK * 4 + 3) # large block
+ # The ICC APP marker can store 65519 bytes per marker, so
+ # using a 4-byte test code should allow us to detect out of
+ # order issues.
+ icc_profile = (b"Test" * int(n / 4 + 1))[:n]
+ assert len(icc_profile) == n # sanity
+ im1 = self.roundtrip(hopper(), icc_profile=icc_profile)
+ assert im1.info.get("icc_profile") == (icc_profile or None)
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@@ -649,11 +652,11 @@ class TestFileJpeg:
# Assert
assert im.format == "JPEG"
- def test_save_correct_modes(self):
+ @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
+ def test_save_correct_modes(self, mode):
out = BytesIO()
- for mode in ["1", "L", "RGB", "RGBX", "CMYK", "YCbCr"]:
- img = Image.new(mode, (20, 20))
- img.save(out, "JPEG")
+ img = Image.new(mode, (20, 20))
+ img.save(out, "JPEG")
def test_save_wrong_modes(self, tmp_path):
# ref https://github.com/python-pillow/Pillow/issues/2005
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 7942d6b9a..cd142e67f 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -126,14 +126,14 @@ def test_prog_res_rt():
assert_image_equal(im, test_card)
-def test_default_num_resolutions():
- for num_resolutions in range(2, 6):
- d = 1 << (num_resolutions - 1)
- im = test_card.resize((d - 1, d - 1))
- with pytest.raises(OSError):
- roundtrip(im, num_resolutions=num_resolutions)
- reloaded = roundtrip(im)
- assert_image_equal(im, reloaded)
+@pytest.mark.parametrize("num_resolutions", range(2, 6))
+def test_default_num_resolutions(num_resolutions):
+ d = 1 << (num_resolutions - 1)
+ im = test_card.resize((d - 1, d - 1))
+ with pytest.raises(OSError):
+ roundtrip(im, num_resolutions=num_resolutions)
+ reloaded = roundtrip(im)
+ assert_image_equal(im, reloaded)
def test_reduce():
@@ -266,14 +266,11 @@ def test_rgba():
assert jp2.mode == "RGBA"
-def test_16bit_monochrome_has_correct_mode():
- with Image.open("Tests/images/16bit.cropped.j2k") as j2k:
- j2k.load()
- assert j2k.mode == "I;16"
-
- with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
- jp2.load()
- assert jp2.mode == "I;16"
+@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
+def test_16bit_monochrome_has_correct_mode(ext):
+ with Image.open("Tests/images/16bit.cropped" + ext) as im:
+ im.load()
+ assert im.mode == "I;16"
def test_16bit_monochrome_jp2_like_tiff():
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index a43548ae0..d9066c589 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -135,50 +135,50 @@ class TestFileLibTiff(LibTiffTestCase):
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"""
- for legacy_api in [False, True]:
- f = str(tmp_path / "temp.tiff")
- with Image.open("Tests/images/hopper_g4.tif") as img:
- img.save(f, tiffinfo=img.tag)
+ f = str(tmp_path / "temp.tiff")
+ with Image.open("Tests/images/hopper_g4.tif") as img:
+ img.save(f, tiffinfo=img.tag)
- if legacy_api:
- original = img.tag.named()
- else:
- original = img.tag_v2.named()
+ if legacy_api:
+ original = img.tag.named()
+ else:
+ original = img.tag_v2.named()
- # PhotometricInterpretation is set from SAVE_INFO,
- # not the original image.
- ignored = [
- "StripByteCounts",
- "RowsPerStrip",
- "PageNumber",
- "PhotometricInterpretation",
- ]
+ # PhotometricInterpretation is set from SAVE_INFO,
+ # not the original image.
+ ignored = [
+ "StripByteCounts",
+ "RowsPerStrip",
+ "PageNumber",
+ "PhotometricInterpretation",
+ ]
- with Image.open(f) as loaded:
- if legacy_api:
- reloaded = loaded.tag.named()
- else:
- reloaded = loaded.tag_v2.named()
+ with Image.open(f) as loaded:
+ if legacy_api:
+ reloaded = loaded.tag.named()
+ else:
+ reloaded = loaded.tag_v2.named()
- for tag, value in itertools.chain(reloaded.items(), original.items()):
- if tag not in ignored:
- val = original[tag]
- if tag.endswith("Resolution"):
- if legacy_api:
- assert val[0][0] / val[0][1] == (
- 4294967295 / 113653537
- ), f"{tag} didn't roundtrip"
- else:
- assert val == 37.79000115940079, f"{tag} didn't roundtrip"
+ for tag, value in itertools.chain(reloaded.items(), original.items()):
+ if tag not in ignored:
+ val = original[tag]
+ if tag.endswith("Resolution"):
+ if legacy_api:
+ assert val[0][0] / val[0][1] == (
+ 4294967295 / 113653537
+ ), f"{tag} didn't roundtrip"
else:
- assert val == value, f"{tag} didn't roundtrip"
+ assert val == 37.79000115940079, f"{tag} didn't roundtrip"
+ else:
+ assert val == value, f"{tag} didn't roundtrip"
- # https://github.com/python-pillow/Pillow/issues/1561
- requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"]
- for field in requested_fields:
- assert field in reloaded, f"{field} not in metadata"
+ # https://github.com/python-pillow/Pillow/issues/1561
+ requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"]
+ for field in requested_fields:
+ assert field in reloaded, f"{field} not in metadata"
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
def test_additional_metadata(self, tmp_path):
@@ -509,20 +509,13 @@ class TestFileLibTiff(LibTiffTestCase):
# colormap/palette tag
assert len(reloaded.tag_v2[320]) == 768
- def xtest_bw_compression_w_rgb(self, tmp_path):
- """This test passes, but when running all tests causes a failure due
- to output on stderr from the error thrown by libtiff. We need to
- capture that but not now"""
-
+ @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
+ def test_bw_compression_w_rgb(self, compression, tmp_path):
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
with pytest.raises(OSError):
- im.save(out, compression="tiff_ccitt")
- with pytest.raises(OSError):
- im.save(out, compression="group3")
- with pytest.raises(OSError):
- im.save(out, compression="group4")
+ im.save(out, compression=compression)
def test_fp_leak(self):
im = Image.open("Tests/images/hopper_g4_500.tif")
@@ -856,7 +849,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_strip_ycbcr_jpeg_2x2_sampling(self):
infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif"
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(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@@ -864,7 +857,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_strip_ycbcr_jpeg_1x1_sampling(self):
infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif"
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):
infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif"
@@ -877,7 +870,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiled_ycbcr_jpeg_1x1_sampling(self):
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif"
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(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@@ -885,7 +878,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiled_ycbcr_jpeg_2x2_sampling(self):
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif"
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):
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
@@ -1011,14 +1004,18 @@ class TestFileLibTiff(LibTiffTestCase):
# Assert that there are multiple strips
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))
out = str(tmp_path / "temp.tif")
- TiffImagePlugin.STRIP_SIZE = 2**18
+ if not argument:
+ TiffImagePlugin.STRIP_SIZE = 2**18
try:
-
- im.save(out, compression="tiff_adobe_deflate")
+ arguments = {"compression": "tiff_adobe_deflate"}
+ if argument:
+ arguments["strip_size"] = 2**18
+ im.save(out, **arguments)
with Image.open(out) as im:
assert len(im.tag_v2[STRIPOFFSETS]) == 1
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index d093f26cc..d94bdaa96 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -5,15 +5,19 @@ import pytest
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"]
pytestmark = skip_unless_feature("jpg")
-def frame_roundtrip(im, **options):
- # Note that for now, there is no MPO saving functionality
+def roundtrip(im, **options):
out = BytesIO()
im.save(out, "MPO", **options)
test_bytes = out.tell()
@@ -23,13 +27,13 @@ def frame_roundtrip(im, **options):
return im
-def test_sanity():
- for test_file in test_files:
- with Image.open(test_file) as im:
- im.load()
- assert im.mode == "RGB"
- assert im.size == (640, 480)
- assert im.format == "MPO"
+@pytest.mark.parametrize("test_file", test_files)
+def test_sanity(test_file):
+ with Image.open(test_file) as im:
+ im.load()
+ assert im.mode == "RGB"
+ assert im.size == (640, 480)
+ assert im.format == "MPO"
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
@@ -62,26 +66,25 @@ def test_context_manager():
im.load()
-def test_app():
- for test_file in test_files:
- # Test APP/COM reader (@PIL135)
- with Image.open(test_file) as im:
- assert im.applist[0][0] == "APP1"
- assert im.applist[1][0] == "APP2"
- assert (
- im.applist[1][1][:16]
- == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
- )
- assert len(im.applist) == 2
+@pytest.mark.parametrize("test_file", test_files)
+def test_app(test_file):
+ # Test APP/COM reader (@PIL135)
+ with Image.open(test_file) as im:
+ assert im.applist[0][0] == "APP1"
+ assert im.applist[1][0] == "APP2"
+ assert (
+ im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
+ )
+ assert len(im.applist) == 2
-def test_exif():
- for test_file in test_files:
- with Image.open(test_file) as im:
- info = im._getexif()
- assert info[272] == "Nintendo 3DS"
- assert info[296] == 2
- assert info[34665] == 188
+@pytest.mark.parametrize("test_file", test_files)
+def test_exif(test_file):
+ with Image.open(test_file) as im:
+ info = im._getexif()
+ assert info[272] == "Nintendo 3DS"
+ assert info[296] == 2
+ assert info[34665] == 188
def test_frame_size():
@@ -133,12 +136,12 @@ def test_reload_exif_after_seek():
assert 296 in exif
-def test_mp():
- for test_file in test_files:
- with Image.open(test_file) as im:
- mpinfo = im._getmp()
- assert mpinfo[45056] == b"0100"
- assert mpinfo[45057] == 2
+@pytest.mark.parametrize("test_file", test_files)
+def test_mp(test_file):
+ with Image.open(test_file) as im:
+ mpinfo = im._getmp()
+ assert mpinfo[45056] == b"0100"
+ assert mpinfo[45057] == 2
def test_mp_offset():
@@ -158,48 +161,48 @@ def test_mp_no_data():
im.seek(1)
-def test_mp_attribute():
- for test_file in test_files:
- with Image.open(test_file) as im:
- mpinfo = im._getmp()
- frame_number = 0
- for mpentry in mpinfo[0xB002]:
- mpattr = mpentry["Attribute"]
- if frame_number:
- assert not mpattr["RepresentativeImageFlag"]
- else:
- assert mpattr["RepresentativeImageFlag"]
- assert not mpattr["DependentParentImageFlag"]
- assert not mpattr["DependentChildImageFlag"]
- assert mpattr["ImageDataFormat"] == "JPEG"
- assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
- assert mpattr["Reserved"] == 0
- frame_number += 1
+@pytest.mark.parametrize("test_file", test_files)
+def test_mp_attribute(test_file):
+ with Image.open(test_file) as im:
+ mpinfo = im._getmp()
+ frame_number = 0
+ for mpentry in mpinfo[0xB002]:
+ mpattr = mpentry["Attribute"]
+ if frame_number:
+ assert not mpattr["RepresentativeImageFlag"]
+ else:
+ assert mpattr["RepresentativeImageFlag"]
+ assert not mpattr["DependentParentImageFlag"]
+ assert not mpattr["DependentChildImageFlag"]
+ assert mpattr["ImageDataFormat"] == "JPEG"
+ assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
+ assert mpattr["Reserved"] == 0
+ frame_number += 1
-def test_seek():
- for test_file in test_files:
- with Image.open(test_file) as im:
- assert im.tell() == 0
- # prior to first image raises an error, both blatant and borderline
- with pytest.raises(EOFError):
- im.seek(-1)
- with pytest.raises(EOFError):
- im.seek(-523)
- # after the final image raises an error,
- # both blatant and borderline
- with pytest.raises(EOFError):
- im.seek(2)
- with pytest.raises(EOFError):
- im.seek(523)
- # bad calls shouldn't change the frame
- assert im.tell() == 0
- # this one will work
- im.seek(1)
- assert im.tell() == 1
- # and this one, too
- im.seek(0)
- assert im.tell() == 0
+@pytest.mark.parametrize("test_file", test_files)
+def test_seek(test_file):
+ with Image.open(test_file) as im:
+ assert im.tell() == 0
+ # prior to first image raises an error, both blatant and borderline
+ with pytest.raises(EOFError):
+ im.seek(-1)
+ with pytest.raises(EOFError):
+ im.seek(-523)
+ # after the final image raises an error,
+ # both blatant and borderline
+ with pytest.raises(EOFError):
+ im.seek(2)
+ with pytest.raises(EOFError):
+ im.seek(523)
+ # bad calls shouldn't change the frame
+ assert im.tell() == 0
+ # this one will work
+ im.seek(1)
+ assert im.tell() == 1
+ # and this one, too
+ im.seek(0)
+ assert im.tell() == 0
def test_n_frames():
@@ -221,29 +224,54 @@ def test_eoferror():
im.seek(n_frames - 1)
-def test_image_grab():
+@pytest.mark.parametrize("test_file", test_files)
+def test_image_grab(test_file):
+ with Image.open(test_file) as im:
+ assert im.tell() == 0
+ im0 = im.tobytes()
+ im.seek(1)
+ assert im.tell() == 1
+ im1 = im.tobytes()
+ im.seek(0)
+ assert im.tell() == 0
+ im02 = im.tobytes()
+ assert im0 == im02
+ assert im0 != im1
+
+
+@pytest.mark.parametrize("test_file", test_files)
+def test_save(test_file):
+ with Image.open(test_file) as im:
+ assert im.tell() == 0
+ jpg0 = roundtrip(im)
+ assert_image_similar(im, jpg0, 30)
+ im.seek(1)
+ assert im.tell() == 1
+ jpg1 = roundtrip(im)
+ assert_image_similar(im, jpg1, 30)
+
+
+def test_save_all():
for test_file in test_files:
with Image.open(test_file) as im:
- assert im.tell() == 0
- im0 = im.tobytes()
- im.seek(1)
- assert im.tell() == 1
- im1 = im.tobytes()
+ im_reloaded = roundtrip(im, save_all=True)
+
im.seek(0)
- assert im.tell() == 0
- im02 = im.tobytes()
- assert im0 == im02
- assert im0 != im1
+ assert_image_similar(im, im_reloaded, 30)
-
-def test_save():
- # Note that only individual frames can be saved at present
- for test_file in test_files:
- with Image.open(test_file) as im:
- assert im.tell() == 0
- jpg0 = frame_roundtrip(im)
- assert_image_similar(im, jpg0, 30)
im.seek(1)
- assert im.tell() == 1
- jpg1 = frame_roundtrip(im)
- assert_image_similar(im, jpg1, 30)
+ 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
diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py
index e1c1c361b..be7c8d0c8 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -63,19 +63,7 @@ def test_p_mode(tmp_path):
roundtrip(tmp_path, mode)
-def test_l_oserror(tmp_path):
- # Arrange
- mode = "L"
-
- # Act / Assert
- with pytest.raises(OSError):
- helper_save_as_palm(tmp_path, mode)
-
-
-def test_rgb_oserror(tmp_path):
- # Arrange
- mode = "RGB"
-
- # Act / Assert
+@pytest.mark.parametrize("mode", ("L", "RGB"))
+def test_oserror(tmp_path, mode):
with pytest.raises(OSError):
helper_save_as_palm(tmp_path, mode)
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index 61e33a57b..485adf785 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -20,6 +20,11 @@ def test_sanity(tmp_path):
for mode in ("1", "L", "P", "RGB"):
_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
f = str(tmp_path / "temp.pcx")
im = hopper("RGBA")
@@ -34,14 +39,14 @@ def test_invalid_file():
PcxImagePlugin.PcxImageFile(invalid_file)
-def test_odd(tmp_path):
+@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
+def test_odd(tmp_path, mode):
# See issue #523, odd sized images should have a stride that's even.
# Not that ImageMagick or GIMP write PCX that way.
# We were not handling properly.
- for mode in ("1", "L", "P", "RGB"):
- # larger, odd sized images are better here to ensure that
- # we handle interrupted scan lines properly.
- _roundtrip(tmp_path, hopper(mode).resize((511, 511)))
+ # larger, odd sized images are better here to ensure that
+ # we handle interrupted scan lines properly.
+ _roundtrip(tmp_path, hopper(mode).resize((511, 511)))
def test_odd_read():
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index c71d4f5f2..4129e8783 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -6,7 +6,7 @@ import time
import pytest
-from PIL import Image, PdfParser
+from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version
@@ -37,45 +37,19 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
return outfile
+@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
+def test_save(tmp_path, mode):
+ helper_save_as_pdf(tmp_path, mode)
+
+
+@pytest.mark.valgrind_known_error(reason="Temporary skip")
def test_monochrome(tmp_path):
# Arrange
mode = "1"
# Act / Assert
outfile = helper_save_as_pdf(tmp_path, mode)
- assert os.path.getsize(outfile) < 15000
-
-
-def test_greyscale(tmp_path):
- # Arrange
- mode = "L"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
-
-
-def test_rgb(tmp_path):
- # Arrange
- mode = "RGB"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
-
-
-def test_p_mode(tmp_path):
- # Arrange
- mode = "P"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
-
-
-def test_cmyk_mode(tmp_path):
- # Arrange
- mode = "CMYK"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
+ assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
def test_unsupported_mode(tmp_path):
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index f3da213b8..112d1444b 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -643,7 +643,9 @@ class TestFilePng:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
- @pytest.mark.parametrize("cid", (b"IHDR", b"pHYs", b"acTL", b"fcTL", b"fdAT"))
+ @pytest.mark.parametrize(
+ "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
+ )
def test_truncated_chunks(self, cid):
fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png:
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index b4b5b7a0c..4f934375c 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -4,7 +4,7 @@ import pytest
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"
@@ -107,6 +107,11 @@ def test_open_after_exclusive_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():
with Image.open(test_file) as im:
assert "icc_profile" in im.info
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index 0c8c9f304..7d8b5139a 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -18,51 +18,48 @@ _ORIGINS = ("tl", "bl")
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
-def test_sanity(tmp_path):
- for mode in _MODES:
+@pytest.mark.parametrize("mode", _MODES)
+def test_sanity(mode, tmp_path):
+ def roundtrip(original_im):
+ out = str(tmp_path / "temp.tga")
- def roundtrip(original_im):
- out = str(tmp_path / "temp.tga")
+ original_im.save(out, rle=rle)
+ with Image.open(out) as saved_im:
+ if rle:
+ assert saved_im.info["compression"] == original_im.info["compression"]
+ assert saved_im.info["orientation"] == original_im.info["orientation"]
+ if mode == "P":
+ assert saved_im.getpalette() == original_im.getpalette()
- original_im.save(out, rle=rle)
- with Image.open(out) as saved_im:
- if rle:
+ assert_image_equal(saved_im, original_im)
+
+ png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png"))
+
+ for png_path in png_paths:
+ with Image.open(png_path) as reference_im:
+ assert reference_im.mode == mode
+
+ path_no_ext = os.path.splitext(png_path)[0]
+ for origin, rle in product(_ORIGINS, (True, False)):
+ tga_path = "{}_{}_{}.tga".format(
+ path_no_ext, origin, "rle" if rle else "raw"
+ )
+
+ with Image.open(tga_path) as original_im:
+ assert original_im.format == "TGA"
+ assert original_im.get_format_mimetype() == "image/x-tga"
+ if rle:
+ assert original_im.info["compression"] == "tga_rle"
assert (
- saved_im.info["compression"] == original_im.info["compression"]
+ original_im.info["orientation"]
+ == _ORIGIN_TO_ORIENTATION[origin]
)
- assert saved_im.info["orientation"] == original_im.info["orientation"]
- if mode == "P":
- assert saved_im.getpalette() == original_im.getpalette()
+ if mode == "P":
+ assert original_im.getpalette() == reference_im.getpalette()
- assert_image_equal(saved_im, original_im)
+ assert_image_equal(original_im, reference_im)
- png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png"))
-
- for png_path in png_paths:
- with Image.open(png_path) as reference_im:
- assert reference_im.mode == mode
-
- path_no_ext = os.path.splitext(png_path)[0]
- for origin, rle in product(_ORIGINS, (True, False)):
- tga_path = "{}_{}_{}.tga".format(
- path_no_ext, origin, "rle" if rle else "raw"
- )
-
- with Image.open(tga_path) as original_im:
- assert original_im.format == "TGA"
- assert original_im.get_format_mimetype() == "image/x-tga"
- if rle:
- assert original_im.info["compression"] == "tga_rle"
- assert (
- original_im.info["orientation"]
- == _ORIGIN_TO_ORIENTATION[origin]
- )
- if mode == "P":
- assert original_im.getpalette() == reference_im.getpalette()
-
- assert_image_equal(original_im, reference_im)
-
- roundtrip(original_im)
+ roundtrip(original_im)
def test_palette_depth_16(tmp_path):
@@ -123,6 +120,18 @@ def test_save(tmp_path):
assert test_im.size == (100, 100)
+def test_small_palette(tmp_path):
+ im = Image.new("P", (1, 1))
+ colors = [0, 0, 0]
+ im.putpalette(colors)
+
+ out = str(tmp_path / "temp.tga")
+ im.save(out)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.getpalette() == colors
+
+
def test_save_wrong_mode(tmp_path):
im = hopper("PA")
out = str(tmp_path / "temp.tga")
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 8706cb950..1a5ba594f 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -84,6 +84,24 @@ class TestFileTiff:
with Image.open("Tests/images/multipage.tiff") as im:
im.load()
+ @pytest.mark.parametrize(
+ "path, sizes",
+ (
+ ("Tests/images/hopper.tif", ()),
+ ("Tests/images/child_ifd.tiff", (16, 8)),
+ ("Tests/images/child_ifd_jpeg.tiff", (20,)),
+ ),
+ )
+ def test_get_child_images(self, path, sizes):
+ with Image.open(path) as im:
+ ims = im.get_child_images()
+
+ assert len(ims) == len(sizes)
+ for i, im in enumerate(ims):
+ w = sizes[i]
+ expected = Image.new("RGB", (w, w), "#f00")
+ assert_image_similar(im, expected, 1)
+
def test_mac_tiff(self):
# Read RGBa images from macOS [@PIL136]
@@ -293,14 +311,17 @@ class TestFileTiff:
with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"):
pass
- def test_n_frames(self):
- for path, n_frames in [
- ["Tests/images/multipage-lastframe.tif", 1],
- ["Tests/images/multipage.tiff", 3],
- ]:
- with Image.open(path) as im:
- assert im.n_frames == n_frames
- assert im.is_animated == (n_frames != 1)
+ @pytest.mark.parametrize(
+ "path, n_frames",
+ (
+ ("Tests/images/multipage-lastframe.tif", 1),
+ ("Tests/images/multipage.tiff", 3),
+ ),
+ )
+ def test_n_frames(self, path, n_frames):
+ with Image.open(path) as im:
+ assert im.n_frames == n_frames
+ assert im.is_animated == (n_frames != 1)
def test_eoferror(self):
with Image.open("Tests/images/multipage-lastframe.tif") as im:
@@ -416,12 +437,12 @@ class TestFileTiff:
len_after = len(dict(im.ifd))
assert len_before == len_after + 1
- def test_load_byte(self):
- for legacy_api in [False, True]:
- ifd = TiffImagePlugin.ImageFileDirectory_v2()
- data = b"abc"
- ret = ifd.load_byte(data, legacy_api)
- assert ret == b"abc"
+ @pytest.mark.parametrize("legacy_api", (False, True))
+ def test_load_byte(self, legacy_api):
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ data = b"abc"
+ ret = ifd.load_byte(data, legacy_api)
+ assert ret == b"abc"
def test_load_string(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
@@ -667,18 +688,15 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)
- def test_palette(self, tmp_path):
- def roundtrip(mode):
- outfile = str(tmp_path / "temp.tif")
+ @pytest.mark.parametrize("mode", ("P", "PA"))
+ def test_palette(self, mode, tmp_path):
+ outfile = str(tmp_path / "temp.tif")
- im = hopper(mode)
- im.save(outfile)
+ im = hopper(mode)
+ im.save(outfile)
- with Image.open(outfile) as reloaded:
- assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
-
- for mode in ["P", "PA"]:
- roundtrip(mode)
+ with Image.open(outfile) as reloaded:
+ assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
def test_tiff_save_all(self):
mp = BytesIO()
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index d7a0d9377..d38c1c523 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -185,6 +185,22 @@ def test_iptc(tmp_path):
im.save(out)
+def test_writing_bytes_to_ascii(tmp_path):
+ im = hopper()
+ info = TiffImagePlugin.ImageFileDirectory_v2()
+
+ tag = TiffTags.TAGS_V2[271]
+ assert tag.type == TiffTags.ASCII
+
+ info[271] = b"test"
+
+ out = str(tmp_path / "temp.tiff")
+ im.save(out, tiffinfo=info)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.tag_v2[271] == "test"
+
+
def test_undefined_zero(tmp_path):
# Check that the tag has not been changed since this test was created
tag = TiffTags.TAGS_V2[45059]
diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py
index d6769a24b..439cb15bc 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -66,10 +66,10 @@ def test_load_set_dpi():
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()
- for ext in [".wmf", ".emf"]:
- tmpfile = str(tmp_path / ("temp" + ext))
- with pytest.raises(OSError):
- im.save(tmpfile)
+ tmpfile = str(tmp_path / ("temp" + ext))
+ with pytest.raises(OSError):
+ im.save(tmpfile)
diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py
index 288848f26..c217378fb 100644
--- a/Tests/test_font_pcf.py
+++ b/Tests/test_font_pcf.py
@@ -49,6 +49,14 @@ def test_sanity(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():
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
@@ -68,12 +76,19 @@ def test_textsize(request, tmp_path):
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
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 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)):
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):
diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py
index a1036fd28..664663fd6 100644
--- a/Tests/test_font_pcf_charsets.py
+++ b/Tests/test_font_pcf_charsets.py
@@ -1,5 +1,7 @@
import os
+import pytest
+
from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
from .helper import (
@@ -59,23 +61,13 @@ def save_font(request, tmp_path, encoding):
return tempname
-def _test_sanity(request, tmp_path, encoding):
+@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
+def test_sanity(request, tmp_path, encoding):
save_font(request, tmp_path, encoding)
-def test_sanity_iso8859_1(request, tmp_path):
- _test_sanity(request, tmp_path, "iso8859-1")
-
-
-def test_sanity_iso8859_2(request, tmp_path):
- _test_sanity(request, tmp_path, "iso8859-2")
-
-
-def test_sanity_cp1250(request, tmp_path):
- _test_sanity(request, tmp_path, "cp1250")
-
-
-def _test_draw(request, tmp_path, encoding):
+@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
+def test_draw(request, tmp_path, encoding):
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
im = Image.new("L", (150, 30), "white")
@@ -85,38 +77,19 @@ def _test_draw(request, tmp_path, encoding):
assert_image_similar_tofile(im, charsets[encoding]["image1"], 0)
-def test_draw_iso8859_1(request, tmp_path):
- _test_draw(request, tmp_path, "iso8859-1")
-
-
-def test_draw_iso8859_2(request, tmp_path):
- _test_draw(request, tmp_path, "iso8859-2")
-
-
-def test_draw_cp1250(request, tmp_path):
- _test_draw(request, tmp_path, "cp1250")
-
-
-def _test_textsize(request, tmp_path, encoding):
+@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
+def test_textsize(request, tmp_path, encoding):
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
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 dx in (0, 10)
+ assert font.getlength(bytearray([i])) == dx
message = charsets[encoding]["message"].encode(encoding)
for i in range(len(message)):
msg = message[: i + 1]
- assert font.getsize(msg) == (len(msg) * 10, 20)
-
-
-def test_textsize_iso8859_1(request, tmp_path):
- _test_textsize(request, tmp_path, "iso8859-1")
-
-
-def test_textsize_iso8859_2(request, tmp_path):
- _test_textsize(request, tmp_path, "iso8859-2")
-
-
-def test_textsize_cp1250(request, tmp_path):
- _test_textsize(request, tmp_path, "cp1250")
+ assert font.getlength(msg) == len(msg) * 10
+ assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20)
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 19986e29d..f67a2da7d 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -29,8 +29,9 @@ from .helper import (
class TestImage:
- def test_image_modes_success(self):
- for mode in [
+ @pytest.mark.parametrize(
+ "mode",
+ (
"1",
"P",
"PA",
@@ -51,22 +52,18 @@ class TestImage:
"YCbCr",
"LAB",
"HSV",
- ]:
- Image.new(mode, (1, 1))
+ ),
+ )
+ def test_image_modes_success(self, mode):
+ Image.new(mode, (1, 1))
- def test_image_modes_fail(self):
- for mode in [
- "",
- "bad",
- "very very long",
- "BGR;15",
- "BGR;16",
- "BGR;24",
- "BGR;32",
- ]:
- with pytest.raises(ValueError) as e:
- Image.new(mode, (1, 1))
- assert str(e.value) == "unrecognized image mode"
+ @pytest.mark.parametrize(
+ "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32")
+ )
+ def test_image_modes_fail(self, mode):
+ with pytest.raises(ValueError) as e:
+ Image.new(mode, (1, 1))
+ assert str(e.value) == "unrecognized image mode"
def test_exception_inheritance(self):
assert issubclass(UnidentifiedImageError, OSError)
@@ -605,23 +602,22 @@ class TestImage:
with pytest.raises(ValueError):
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
target_file = "Tests/images/linear_gradient.png"
- for mode in ["L", "P", "I", "F"]:
- # Act
- im = Image.linear_gradient(mode)
+ # Act
+ im = Image.linear_gradient(mode)
- # Assert
- assert im.size == (256, 256)
- assert im.mode == mode
- assert im.getpixel((0, 0)) == 0
- assert im.getpixel((255, 255)) == 255
- with Image.open(target_file) as target:
- target = target.convert(mode)
- assert_image_equal(im, target)
+ # Assert
+ assert im.size == (256, 256)
+ assert im.mode == mode
+ assert im.getpixel((0, 0)) == 0
+ assert im.getpixel((255, 255)) == 255
+ with Image.open(target_file) as target:
+ target = target.convert(mode)
+ assert_image_equal(im, target)
def test_radial_gradient_wrong_mode(self):
# Arrange
@@ -631,23 +627,22 @@ class TestImage:
with pytest.raises(ValueError):
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
target_file = "Tests/images/radial_gradient.png"
- for mode in ["L", "P", "I", "F"]:
- # Act
- im = Image.radial_gradient(mode)
+ # Act
+ im = Image.radial_gradient(mode)
- # Assert
- assert im.size == (256, 256)
- assert im.mode == mode
- assert im.getpixel((0, 0)) == 255
- assert im.getpixel((128, 128)) == 0
- with Image.open(target_file) as target:
- target = target.convert(mode)
- assert_image_equal(im, target)
+ # Assert
+ assert im.size == (256, 256)
+ assert im.mode == mode
+ assert im.getpixel((0, 0)) == 255
+ assert im.getpixel((128, 128)) == 0
+ with Image.open(target_file) as target:
+ target = target.convert(mode)
+ assert_image_equal(im, target)
def test_register_extensions(self):
test_format = "a"
@@ -691,6 +686,7 @@ class TestImage:
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
+ assert len(im_remapped.getpalette()) == 6
# Test unused transparency
im.info["transparency"] = 2
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 617274a57..bb09a7708 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -184,8 +184,9 @@ class TestImageGetPixel(AccessTest):
with pytest.raises(error):
im.getpixel((-1, -1))
- def test_basic(self):
- for mode in (
+ @pytest.mark.parametrize(
+ "mode",
+ (
"1",
"L",
"LA",
@@ -200,23 +201,28 @@ class TestImageGetPixel(AccessTest):
"RGBX",
"CMYK",
"YCbCr",
- ):
- self.check(mode)
+ ),
+ )
+ def test_basic(self, mode):
+ self.check(mode)
- def test_signedness(self):
+ @pytest.mark.parametrize("mode", ("I;16", "I;16B"))
+ def test_signedness(self, mode):
# see https://github.com/python-pillow/Pillow/issues/452
# 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)
- self.check(mode, 2**15 + 1)
- self.check(mode, 2**16 - 1)
+ self.check(mode, 2**15 - 1)
+ self.check(mode, 2**15)
+ self.check(mode, 2**15 + 1)
+ self.check(mode, 2**16 - 1)
- def test_p_putpixel_rgb_rgba(self):
- for color in [(255, 0, 0), (255, 0, 0, 255)]:
- im = Image.new("P", (1, 1), 0)
- im.putpixel((0, 0), color)
- assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
+ @pytest.mark.parametrize("mode", ("P", "PA"))
+ @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
+ def test_p_putpixel_rgb_rgba(self, mode, color):
+ im = Image.new(mode, (1, 1))
+ im.putpixel((0, 0), color)
+
+ alpha = color[3] if len(color) == 4 and mode == "PA" else 255
+ assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.skipif(cffi is None, reason="No CFFI")
@@ -337,12 +343,15 @@ class TestCffi(AccessTest):
# pixels can contain garbage if image is released
assert px[i, 0] == 0
- def test_p_putpixel_rgb_rgba(self):
- for color in [(255, 0, 0), (255, 0, 0, 255)]:
- im = Image.new("P", (1, 1), 0)
+ @pytest.mark.parametrize("mode", ("P", "PA"))
+ def test_p_putpixel_rgb_rgba(self, mode):
+ for color in [(255, 0, 0), (255, 0, 0, 127)]:
+ im = Image.new(mode, (1, 1))
access = PyAccess.new(im, False)
access.putpixel((0, 0), color)
- assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
+
+ alpha = color[3] if len(color) == 4 and mode == "PA" else 255
+ assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
class TestImagePutPixelError(AccessTest):
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index 7168c4265..7e5fd6fe1 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -1,4 +1,5 @@
import pytest
+from packaging.version import parse as parse_version
from PIL import Image
@@ -34,9 +35,10 @@ def test_toarray():
test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8)
- with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
- with pytest.raises(OSError):
- numpy.array(im_truncated)
+ if parse_version(numpy.__version__) >= parse_version("1.23"):
+ with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
+ with pytest.raises(OSError):
+ numpy.array(im_truncated)
def test_fromarray():
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index e5639e105..1a78f8b4c 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -236,6 +236,12 @@ def test_p2pa_alpha():
assert im_a.getpixel((x, y)) == alpha
+def test_p2pa_palette():
+ with Image.open("Tests/images/tiny.png") as im:
+ im_pa = im.convert("PA")
+ assert im_pa.getpalette() == im.getpalette()
+
+
def test_matrix_illegal_conversion():
# Arrange
im = hopper("CMYK")
@@ -268,36 +274,33 @@ def test_matrix_wrong_mode():
im.convert(mode="L", matrix=matrix)
-def test_matrix_xyz():
- def matrix_convert(mode):
- # Arrange
- im = hopper("RGB")
- im.info["transparency"] = (255, 0, 0)
- # fmt: off
- matrix = (
- 0.412453, 0.357580, 0.180423, 0,
- 0.212671, 0.715160, 0.072169, 0,
- 0.019334, 0.119193, 0.950227, 0)
- # fmt: on
- assert im.mode == "RGB"
+@pytest.mark.parametrize("mode", ("RGB", "L"))
+def test_matrix_xyz(mode):
+ # Arrange
+ im = hopper("RGB")
+ im.info["transparency"] = (255, 0, 0)
+ # fmt: off
+ matrix = (
+ 0.412453, 0.357580, 0.180423, 0,
+ 0.212671, 0.715160, 0.072169, 0,
+ 0.019334, 0.119193, 0.950227, 0)
+ # fmt: on
+ assert im.mode == "RGB"
- # Act
- # Convert an RGB image to the CIE XYZ colour space
- converted_im = im.convert(mode=mode, matrix=matrix)
+ # Act
+ # Convert an RGB image to the CIE XYZ colour space
+ converted_im = im.convert(mode=mode, matrix=matrix)
- # Assert
- assert converted_im.mode == mode
- assert converted_im.size == im.size
- with Image.open("Tests/images/hopper-XYZ.png") as target:
- if converted_im.mode == "RGB":
- assert_image_similar(converted_im, target, 3)
- assert converted_im.info["transparency"] == (105, 54, 4)
- else:
- assert_image_similar(converted_im, target.getchannel(0), 1)
- assert converted_im.info["transparency"] == 105
-
- matrix_convert("RGB")
- matrix_convert("L")
+ # Assert
+ assert converted_im.mode == mode
+ assert converted_im.size == im.size
+ with Image.open("Tests/images/hopper-XYZ.png") as target:
+ if converted_im.mode == "RGB":
+ assert_image_similar(converted_im, target, 3)
+ assert converted_im.info["transparency"] == (105, 54, 4)
+ else:
+ assert_image_similar(converted_im, target.getchannel(0), 1)
+ assert converted_im.info["transparency"] == 105
def test_matrix_identity():
diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py
index 21e438654..591832147 100644
--- a/Tests/test_image_copy.py
+++ b/Tests/test_image_copy.py
@@ -1,37 +1,40 @@
import copy
+import pytest
+
from PIL import Image
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_size = (10, 10)
- for mode in "1", "P", "L", "RGB", "I", "F":
- # Internal copy method
- im = hopper(mode)
- out = im.copy()
- assert out.mode == im.mode
- assert out.size == im.size
- # Python's copy method
- im = hopper(mode)
- out = copy.copy(im)
- assert out.mode == im.mode
- assert out.size == im.size
+ # Internal copy method
+ im = hopper(mode)
+ out = im.copy()
+ assert out.mode == im.mode
+ assert out.size == im.size
- # Internal copy method on a cropped image
- im = hopper(mode)
- out = im.crop(cropped_coordinates).copy()
- assert out.mode == im.mode
- assert out.size == cropped_size
+ # Python's copy method
+ im = hopper(mode)
+ out = copy.copy(im)
+ assert out.mode == im.mode
+ assert out.size == im.size
- # Python's copy method on a cropped image
- im = hopper(mode)
- out = copy.copy(im.crop(cropped_coordinates))
- assert out.mode == im.mode
- assert out.size == cropped_size
+ # Internal copy method on a cropped image
+ im = hopper(mode)
+ out = im.crop(cropped_coordinates).copy()
+ assert out.mode == im.mode
+ assert out.size == cropped_size
+
+ # Python's copy method on a cropped image
+ im = hopper(mode)
+ out = copy.copy(im.crop(cropped_coordinates))
+ assert out.mode == im.mode
+ assert out.size == cropped_size
def test_copy_zero():
diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py
index 6574e6efd..4aa41de27 100644
--- a/Tests/test_image_crop.py
+++ b/Tests/test_image_crop.py
@@ -5,17 +5,14 @@ from PIL import Image
from .helper import assert_image_equal, hopper
-def test_crop():
- def crop(mode):
- im = hopper(mode)
- assert_image_equal(im.crop(), im)
+@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
+def test_crop(mode):
+ im = hopper(mode)
+ assert_image_equal(im.crop(), im)
- cropped = im.crop((50, 50, 100, 100))
- assert cropped.mode == mode
- assert cropped.size == (50, 50)
-
- for mode in "1", "P", "L", "RGB", "I", "F":
- crop(mode)
+ cropped = im.crop((50, 50, 100, 100))
+ assert cropped.mode == mode
+ assert cropped.size == (50, 50)
def test_wide_crop():
diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py
index 876d676fe..ea5886e72 100644
--- a/Tests/test_image_entropy.py
+++ b/Tests/test_image_entropy.py
@@ -9,7 +9,7 @@ def test_entropy():
assert round(abs(entropy("L") - 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("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("RGBA") - 7.42724306524488), 7) == 0
assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index 14a8da9f1..cfe46b658 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -5,90 +5,109 @@ from PIL import Image, ImageFilter
from .helper import assert_image_equal, hopper
-def test_sanity():
- def apply_filter(filter_to_apply):
- for mode in ["L", "RGB", "CMYK"]:
- im = hopper(mode)
- out = im.filter(filter_to_apply)
- assert out.mode == im.mode
- assert out.size == im.size
+@pytest.mark.parametrize(
+ "filter_to_apply",
+ (
+ ImageFilter.BLUR,
+ ImageFilter.CONTOUR,
+ ImageFilter.DETAIL,
+ ImageFilter.EDGE_ENHANCE,
+ ImageFilter.EDGE_ENHANCE_MORE,
+ ImageFilter.EMBOSS,
+ ImageFilter.FIND_EDGES,
+ ImageFilter.SMOOTH,
+ ImageFilter.SMOOTH_MORE,
+ ImageFilter.SHARPEN,
+ ImageFilter.MaxFilter,
+ ImageFilter.MedianFilter,
+ ImageFilter.MinFilter,
+ ImageFilter.ModeFilter,
+ ImageFilter.GaussianBlur,
+ ImageFilter.GaussianBlur(5),
+ ImageFilter.BoxBlur(5),
+ ImageFilter.UnsharpMask,
+ ImageFilter.UnsharpMask(10),
+ ),
+)
+@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
+def test_sanity(filter_to_apply, mode):
+ im = hopper(mode)
+ out = im.filter(filter_to_apply)
+ assert out.mode == im.mode
+ assert out.size == im.size
- apply_filter(ImageFilter.BLUR)
- apply_filter(ImageFilter.CONTOUR)
- apply_filter(ImageFilter.DETAIL)
- apply_filter(ImageFilter.EDGE_ENHANCE)
- apply_filter(ImageFilter.EDGE_ENHANCE_MORE)
- apply_filter(ImageFilter.EMBOSS)
- apply_filter(ImageFilter.FIND_EDGES)
- apply_filter(ImageFilter.SMOOTH)
- apply_filter(ImageFilter.SMOOTH_MORE)
- apply_filter(ImageFilter.SHARPEN)
- apply_filter(ImageFilter.MaxFilter)
- apply_filter(ImageFilter.MedianFilter)
- apply_filter(ImageFilter.MinFilter)
- apply_filter(ImageFilter.ModeFilter)
- apply_filter(ImageFilter.GaussianBlur)
- apply_filter(ImageFilter.GaussianBlur(5))
- apply_filter(ImageFilter.BoxBlur(5))
- apply_filter(ImageFilter.UnsharpMask)
- apply_filter(ImageFilter.UnsharpMask(10))
+@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
+def test_sanity_error(mode):
with pytest.raises(TypeError):
- apply_filter("hello")
+ im = hopper(mode)
+ im.filter("hello")
-def test_crash():
-
- # crashes on small images
- im = Image.new("RGB", (1, 1))
- im.filter(ImageFilter.SMOOTH)
-
- im = Image.new("RGB", (2, 2))
- im.filter(ImageFilter.SMOOTH)
-
- im = Image.new("RGB", (3, 3))
+# crashes on small images
+@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
+def test_crash(size):
+ im = Image.new("RGB", size)
im.filter(ImageFilter.SMOOTH)
-def test_modefilter():
- def modefilter(mode):
- im = Image.new(mode, (3, 3), None)
- im.putdata(list(range(9)))
- # image is:
- # 0 1 2
- # 3 4 5
- # 6 7 8
- mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
- im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0
- mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
- return mod, mod2
-
- assert modefilter("1") == (4, 0)
- assert modefilter("L") == (4, 0)
- assert modefilter("P") == (4, 0)
- assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0))
+@pytest.mark.parametrize(
+ "mode, expected",
+ (
+ ("1", (4, 0)),
+ ("L", (4, 0)),
+ ("P", (4, 0)),
+ ("RGB", ((4, 0, 0), (0, 0, 0))),
+ ),
+)
+def test_modefilter(mode, expected):
+ im = Image.new(mode, (3, 3), None)
+ im.putdata(list(range(9)))
+ # image is:
+ # 0 1 2
+ # 3 4 5
+ # 6 7 8
+ mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
+ im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0
+ mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
+ assert (mod, mod2) == expected
-def test_rankfilter():
- def rankfilter(mode):
- im = Image.new(mode, (3, 3), None)
- im.putdata(list(range(9)))
- # image is:
- # 0 1 2
- # 3 4 5
- # 6 7 8
- minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1))
- med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1))
- maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1))
- return minimum, med, maximum
+@pytest.mark.parametrize(
+ "mode, expected",
+ (
+ ("1", (0, 4, 8)),
+ ("L", (0, 4, 8)),
+ ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))),
+ ("I", (0, 4, 8)),
+ ("F", (0.0, 4.0, 8.0)),
+ ),
+)
+def test_rankfilter(mode, expected):
+ im = Image.new(mode, (3, 3), None)
+ im.putdata(list(range(9)))
+ # image is:
+ # 0 1 2
+ # 3 4 5
+ # 6 7 8
+ minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1))
+ med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1))
+ maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1))
+ assert (minimum, med, maximum) == expected
- assert rankfilter("1") == (0, 4, 8)
- assert rankfilter("L") == (0, 4, 8)
+
+@pytest.mark.parametrize(
+ "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
+)
+def test_rankfilter_error(filter):
with pytest.raises(ValueError):
- rankfilter("P")
- assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0))
- assert rankfilter("I") == (0, 4, 8)
- assert rankfilter("F") == (0.0, 4.0, 8.0)
+ im = Image.new("P", (3, 3), None)
+ im.putdata(list(range(9)))
+ # image is:
+ # 0 1 2
+ # 3 4 5
+ # 6 7 8
+ im.filter(filter).getpixel((1, 1))
def test_rankfilter_properties():
@@ -110,7 +129,8 @@ def test_kernel_not_enough_coefficients():
ImageFilter.Kernel((3, 3), (0, 0))
-def test_consistency_3x3():
+@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
+def test_consistency_3x3(mode):
with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss.bmp") as reference:
kernel = ImageFilter.Kernel(
@@ -125,14 +145,14 @@ def test_consistency_3x3():
source = source.split() * 2
reference = reference.split() * 2
- for mode in ["L", "LA", "RGB", "CMYK"]:
- assert_image_equal(
- Image.merge(mode, source[: len(mode)]).filter(kernel),
- Image.merge(mode, reference[: len(mode)]),
- )
+ assert_image_equal(
+ Image.merge(mode, source[: len(mode)]).filter(kernel),
+ Image.merge(mode, reference[: len(mode)]),
+ )
-def test_consistency_5x5():
+@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
+def test_consistency_5x5(mode):
with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
kernel = ImageFilter.Kernel(
@@ -149,8 +169,7 @@ def test_consistency_5x5():
source = source.split() * 2
reference = reference.split() * 2
- for mode in ["L", "LA", "RGB", "CMYK"]:
- assert_image_equal(
- Image.merge(mode, source[: len(mode)]).filter(kernel),
- Image.merge(mode, reference[: len(mode)]),
- )
+ assert_image_equal(
+ Image.merge(mode, source[: len(mode)]).filter(kernel),
+ Image.merge(mode, reference[: len(mode)]),
+ )
diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py
index e5b6a7724..7fd0398f9 100644
--- a/Tests/test_image_getcolors.py
+++ b/Tests/test_image_getcolors.py
@@ -16,7 +16,7 @@ def test_getcolors():
assert getcolors("L") == 255
assert getcolors("I") == 255
assert getcolors("F") == 255
- assert getcolors("P") == 90 # fixed palette
+ assert getcolors("P") == 96 # fixed palette
assert getcolors("RGB") is None
assert getcolors("RGBA") is None
assert getcolors("CMYK") is None
diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py
index 91e02973d..0ee52e724 100644
--- a/Tests/test_image_histogram.py
+++ b/Tests/test_image_histogram.py
@@ -10,7 +10,7 @@ def test_histogram():
assert histogram("L") == (256, 0, 662)
assert histogram("I") == (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("RGBA") == (1024, 0, 16384)
assert histogram("CMYK") == (1024, 0, 16384)
diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py
index 4ea1d73ce..1ab02017d 100644
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image
from .helper import CachedProperty, assert_image_equal
@@ -101,226 +103,226 @@ class TestImagingPaste:
],
)
- def test_image_solid(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), "red")
- im2 = getattr(self, "gradient_" + mode)
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_image_solid(self, mode):
+ im = Image.new(mode, (200, 200), "red")
+ im2 = getattr(self, "gradient_" + mode)
- im.paste(im2, (12, 23))
+ im.paste(im2, (12, 23))
- im = im.crop((12, 23, im2.width + 12, im2.height + 23))
- assert_image_equal(im, im2)
+ im = im.crop((12, 23, im2.width + 12, im2.height + 23))
+ assert_image_equal(im, im2)
- def test_image_mask_1(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), "white")
- im2 = getattr(self, "gradient_" + mode)
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_image_mask_1(self, mode):
+ im = Image.new(mode, (200, 200), "white")
+ im2 = getattr(self, "gradient_" + mode)
- self.assert_9points_paste(
- im,
- im2,
- self.mask_1,
- [
- (255, 255, 255, 255),
- (255, 255, 255, 255),
- (127, 254, 127, 0),
- (255, 255, 255, 255),
- (255, 255, 255, 255),
- (191, 190, 63, 64),
- (127, 0, 127, 254),
- (191, 64, 63, 190),
- (255, 255, 255, 255),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ im2,
+ self.mask_1,
+ [
+ (255, 255, 255, 255),
+ (255, 255, 255, 255),
+ (127, 254, 127, 0),
+ (255, 255, 255, 255),
+ (255, 255, 255, 255),
+ (191, 190, 63, 64),
+ (127, 0, 127, 254),
+ (191, 64, 63, 190),
+ (255, 255, 255, 255),
+ ],
+ )
- def test_image_mask_L(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), "white")
- im2 = getattr(self, "gradient_" + mode)
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_image_mask_L(self, mode):
+ im = Image.new(mode, (200, 200), "white")
+ im2 = getattr(self, "gradient_" + mode)
- self.assert_9points_paste(
- im,
- im2,
- self.mask_L,
- [
- (128, 191, 255, 191),
- (208, 239, 239, 208),
- (255, 255, 255, 255),
- (112, 111, 206, 207),
- (192, 191, 191, 191),
- (239, 239, 207, 207),
- (128, 1, 128, 254),
- (207, 113, 112, 207),
- (255, 191, 128, 191),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ im2,
+ self.mask_L,
+ [
+ (128, 191, 255, 191),
+ (208, 239, 239, 208),
+ (255, 255, 255, 255),
+ (112, 111, 206, 207),
+ (192, 191, 191, 191),
+ (239, 239, 207, 207),
+ (128, 1, 128, 254),
+ (207, 113, 112, 207),
+ (255, 191, 128, 191),
+ ],
+ )
- def test_image_mask_LA(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), "white")
- im2 = getattr(self, "gradient_" + mode)
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_image_mask_LA(self, mode):
+ im = Image.new(mode, (200, 200), "white")
+ im2 = getattr(self, "gradient_" + mode)
- self.assert_9points_paste(
- im,
- im2,
- self.gradient_LA,
- [
- (128, 191, 255, 191),
- (112, 207, 206, 111),
- (128, 254, 128, 1),
- (208, 208, 239, 239),
- (192, 191, 191, 191),
- (207, 207, 112, 113),
- (255, 255, 255, 255),
- (239, 207, 207, 239),
- (255, 191, 128, 191),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ im2,
+ self.gradient_LA,
+ [
+ (128, 191, 255, 191),
+ (112, 207, 206, 111),
+ (128, 254, 128, 1),
+ (208, 208, 239, 239),
+ (192, 191, 191, 191),
+ (207, 207, 112, 113),
+ (255, 255, 255, 255),
+ (239, 207, 207, 239),
+ (255, 191, 128, 191),
+ ],
+ )
- def test_image_mask_RGBA(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), "white")
- im2 = getattr(self, "gradient_" + mode)
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_image_mask_RGBA(self, mode):
+ im = Image.new(mode, (200, 200), "white")
+ im2 = getattr(self, "gradient_" + mode)
- self.assert_9points_paste(
- im,
- im2,
- self.gradient_RGBA,
- [
- (128, 191, 255, 191),
- (208, 239, 239, 208),
- (255, 255, 255, 255),
- (112, 111, 206, 207),
- (192, 191, 191, 191),
- (239, 239, 207, 207),
- (128, 1, 128, 254),
- (207, 113, 112, 207),
- (255, 191, 128, 191),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ im2,
+ self.gradient_RGBA,
+ [
+ (128, 191, 255, 191),
+ (208, 239, 239, 208),
+ (255, 255, 255, 255),
+ (112, 111, 206, 207),
+ (192, 191, 191, 191),
+ (239, 239, 207, 207),
+ (128, 1, 128, 254),
+ (207, 113, 112, 207),
+ (255, 191, 128, 191),
+ ],
+ )
- def test_image_mask_RGBa(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), "white")
- im2 = getattr(self, "gradient_" + mode)
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_image_mask_RGBa(self, mode):
+ im = Image.new(mode, (200, 200), "white")
+ im2 = getattr(self, "gradient_" + mode)
- self.assert_9points_paste(
- im,
- im2,
- self.gradient_RGBa,
- [
- (128, 255, 126, 255),
- (0, 127, 126, 255),
- (126, 253, 126, 255),
- (128, 127, 254, 255),
- (0, 255, 254, 255),
- (126, 125, 254, 255),
- (128, 1, 128, 255),
- (0, 129, 128, 255),
- (126, 255, 128, 255),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ im2,
+ self.gradient_RGBa,
+ [
+ (128, 255, 126, 255),
+ (0, 127, 126, 255),
+ (126, 253, 126, 255),
+ (128, 127, 254, 255),
+ (0, 255, 254, 255),
+ (126, 125, 254, 255),
+ (128, 1, 128, 255),
+ (0, 129, 128, 255),
+ (126, 255, 128, 255),
+ ],
+ )
- def test_color_solid(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), "black")
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_color_solid(self, mode):
+ im = Image.new(mode, (200, 200), "black")
- rect = (12, 23, 128 + 12, 128 + 23)
- im.paste("white", rect)
+ rect = (12, 23, 128 + 12, 128 + 23)
+ im.paste("white", rect)
- hist = im.crop(rect).histogram()
- while hist:
- head, hist = hist[:256], hist[256:]
- assert head[255] == 128 * 128
- assert sum(head[:255]) == 0
+ hist = im.crop(rect).histogram()
+ while hist:
+ head, hist = hist[:256], hist[256:]
+ assert head[255] == 128 * 128
+ assert sum(head[:255]) == 0
- def test_color_mask_1(self):
- for mode in ("RGBA", "RGB", "L"):
- im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
- color = (10, 20, 30, 40)[: len(mode)]
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_color_mask_1(self, mode):
+ im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
+ color = (10, 20, 30, 40)[: len(mode)]
- self.assert_9points_paste(
- im,
- color,
- self.mask_1,
- [
- (50, 60, 70, 80),
- (50, 60, 70, 80),
- (10, 20, 30, 40),
- (50, 60, 70, 80),
- (50, 60, 70, 80),
- (10, 20, 30, 40),
- (10, 20, 30, 40),
- (10, 20, 30, 40),
- (50, 60, 70, 80),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ color,
+ self.mask_1,
+ [
+ (50, 60, 70, 80),
+ (50, 60, 70, 80),
+ (10, 20, 30, 40),
+ (50, 60, 70, 80),
+ (50, 60, 70, 80),
+ (10, 20, 30, 40),
+ (10, 20, 30, 40),
+ (10, 20, 30, 40),
+ (50, 60, 70, 80),
+ ],
+ )
- def test_color_mask_L(self):
- for mode in ("RGBA", "RGB", "L"):
- im = getattr(self, "gradient_" + mode).copy()
- color = "white"
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_color_mask_L(self, mode):
+ im = getattr(self, "gradient_" + mode).copy()
+ color = "white"
- self.assert_9points_paste(
- im,
- color,
- self.mask_L,
- [
- (127, 191, 254, 191),
- (111, 207, 206, 110),
- (127, 254, 127, 0),
- (207, 207, 239, 239),
- (191, 191, 190, 191),
- (207, 206, 111, 112),
- (254, 254, 254, 255),
- (239, 206, 206, 238),
- (254, 191, 127, 191),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ color,
+ self.mask_L,
+ [
+ (127, 191, 254, 191),
+ (111, 207, 206, 110),
+ (127, 254, 127, 0),
+ (207, 207, 239, 239),
+ (191, 191, 190, 191),
+ (207, 206, 111, 112),
+ (254, 254, 254, 255),
+ (239, 206, 206, 238),
+ (254, 191, 127, 191),
+ ],
+ )
- def test_color_mask_RGBA(self):
- for mode in ("RGBA", "RGB", "L"):
- im = getattr(self, "gradient_" + mode).copy()
- color = "white"
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_color_mask_RGBA(self, mode):
+ im = getattr(self, "gradient_" + mode).copy()
+ color = "white"
- self.assert_9points_paste(
- im,
- color,
- self.gradient_RGBA,
- [
- (127, 191, 254, 191),
- (111, 207, 206, 110),
- (127, 254, 127, 0),
- (207, 207, 239, 239),
- (191, 191, 190, 191),
- (207, 206, 111, 112),
- (254, 254, 254, 255),
- (239, 206, 206, 238),
- (254, 191, 127, 191),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ color,
+ self.gradient_RGBA,
+ [
+ (127, 191, 254, 191),
+ (111, 207, 206, 110),
+ (127, 254, 127, 0),
+ (207, 207, 239, 239),
+ (191, 191, 190, 191),
+ (207, 206, 111, 112),
+ (254, 254, 254, 255),
+ (239, 206, 206, 238),
+ (254, 191, 127, 191),
+ ],
+ )
- def test_color_mask_RGBa(self):
- for mode in ("RGBA", "RGB", "L"):
- im = getattr(self, "gradient_" + mode).copy()
- color = "white"
+ @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
+ def test_color_mask_RGBa(self, mode):
+ im = getattr(self, "gradient_" + mode).copy()
+ color = "white"
- self.assert_9points_paste(
- im,
- color,
- self.gradient_RGBa,
- [
- (255, 63, 126, 63),
- (47, 143, 142, 46),
- (126, 253, 126, 255),
- (15, 15, 47, 47),
- (63, 63, 62, 63),
- (142, 141, 46, 47),
- (255, 255, 255, 0),
- (48, 15, 15, 47),
- (126, 63, 255, 63),
- ],
- )
+ self.assert_9points_paste(
+ im,
+ color,
+ self.gradient_RGBa,
+ [
+ (255, 63, 126, 63),
+ (47, 143, 142, 46),
+ (126, 253, 126, 255),
+ (15, 15, 47, 47),
+ (63, 63, 62, 63),
+ (142, 141, 46, 47),
+ (255, 255, 255, 0),
+ (48, 15, 15, 47),
+ (126, 63, 255, 63),
+ ],
+ )
def test_different_sizes(self):
im = Image.new("RGB", (100, 100))
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
index e9afd9118..981753eb9 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -65,6 +65,22 @@ def test_quantize_no_dither():
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():
image = hopper()
with Image.open("Tests/images/caption_6_33_22.png") as palette:
diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py
index 70dc87f0a..ae8d740a0 100644
--- a/Tests/test_image_reduce.py
+++ b/Tests/test_image_reduce.py
@@ -38,58 +38,64 @@ gradients_image = Image.open("Tests/images/radial_gradients.png")
gradients_image.load()
-def test_args_factor():
+@pytest.mark.parametrize(
+ "size, expected",
+ (
+ (3, (4, 4)),
+ ((3, 1), (4, 10)),
+ ((1, 3), (10, 4)),
+ ),
+)
+def test_args_factor(size, expected):
im = Image.new("L", (10, 10))
-
- assert (4, 4) == im.reduce(3).size
- assert (4, 10) == im.reduce((3, 1)).size
- assert (10, 4) == im.reduce((1, 3)).size
-
- with pytest.raises(ValueError):
- im.reduce(0)
- with pytest.raises(TypeError):
- im.reduce(2.0)
- with pytest.raises(ValueError):
- im.reduce((0, 10))
+ assert expected == im.reduce(size).size
-def test_args_box():
+@pytest.mark.parametrize(
+ "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
+)
+def test_args_factor_error(size, expected_error):
im = Image.new("L", (10, 10))
-
- assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size
- assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size
-
- with pytest.raises(TypeError):
- im.reduce(2, "stri")
- with pytest.raises(TypeError):
- im.reduce(2, 2)
- with pytest.raises(ValueError):
- im.reduce(2, (0, 0, 11, 10))
- with pytest.raises(ValueError):
- im.reduce(2, (0, 0, 10, 11))
- with pytest.raises(ValueError):
- im.reduce(2, (-1, 0, 10, 10))
- with pytest.raises(ValueError):
- im.reduce(2, (0, -1, 10, 10))
- with pytest.raises(ValueError):
- im.reduce(2, (0, 5, 10, 5))
- with pytest.raises(ValueError):
- im.reduce(2, (5, 0, 5, 10))
+ with pytest.raises(expected_error):
+ im.reduce(size)
-def test_unsupported_modes():
+@pytest.mark.parametrize(
+ "size, expected",
+ (
+ ((0, 0, 10, 10), (5, 5)),
+ ((5, 5, 6, 6), (1, 1)),
+ ),
+)
+def test_args_box(size, expected):
+ im = Image.new("L", (10, 10))
+ assert expected == im.reduce(2, size).size
+
+
+@pytest.mark.parametrize(
+ "size, expected_error",
+ (
+ ("stri", TypeError),
+ ((0, 0, 11, 10), ValueError),
+ ((0, 0, 10, 11), ValueError),
+ ((-1, 0, 10, 10), ValueError),
+ ((0, -1, 10, 10), ValueError),
+ ((0, 5, 10, 5), ValueError),
+ ((5, 0, 5, 10), ValueError),
+ ),
+)
+def test_args_box_error(size, expected_error):
+ im = Image.new("L", (10, 10))
+ with pytest.raises(expected_error):
+ im.reduce(2, size).size
+
+
+@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
+def test_unsupported_modes(mode):
im = Image.new("P", (10, 10))
with pytest.raises(ValueError):
im.reduce(3)
- im = Image.new("1", (10, 10))
- with pytest.raises(ValueError):
- im.reduce(3)
-
- im = Image.new("I;16", (10, 10))
- with pytest.raises(ValueError):
- im.reduce(3)
-
def get_image(mode):
mode_info = ImageMode.getmode(mode)
@@ -190,70 +196,76 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255):
)
-def test_mode_L():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_L(factor):
im = get_image("L")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
-def test_mode_LA():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_LA(factor):
im = get_image("LA")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor, 0.8, 5)
+ compare_reduce_with_reference(im, factor, 0.8, 5)
+
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_LA_opaque(factor):
+ im = get_image("LA")
# With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255))
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
-def test_mode_La():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_La(factor):
im = get_image("La")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
-def test_mode_RGB():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_RGB(factor):
im = get_image("RGB")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
-def test_mode_RGBA():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_RGBA(factor):
im = get_image("RGBA")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor, 0.8, 5)
+ compare_reduce_with_reference(im, factor, 0.8, 5)
+
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_RGBA_opaque(factor):
+ im = get_image("RGBA")
# With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255))
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
-def test_mode_RGBa():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_RGBa(factor):
im = get_image("RGBa")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
-def test_mode_I():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_I(factor):
im = get_image("I")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
-def test_mode_F():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_F(factor):
im = get_image("F")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor, 0, 0)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor, 0, 0)
+ compare_reduce_with_box(im, factor)
@skip_unless_feature("jpg_2000")
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index 6d050efcc..53ceb6df0 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -100,40 +100,41 @@ class TestImagingCoreResampleAccuracy:
for y in range(image.size[1])
)
- def test_reduce_box(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (8, 8), 0xE1)
- case = case.resize((4, 4), Image.Resampling.BOX)
- # fmt: off
- data = ("e1 e1"
- "e1 e1")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (4, 4)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_reduce_box(self, mode):
+ case = self.make_case(mode, (8, 8), 0xE1)
+ case = case.resize((4, 4), Image.Resampling.BOX)
+ # fmt: off
+ data = ("e1 e1"
+ "e1 e1")
+ # fmt: on
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (4, 4)))
- def test_reduce_bilinear(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (8, 8), 0xE1)
- case = case.resize((4, 4), Image.Resampling.BILINEAR)
- # fmt: off
- data = ("e1 c9"
- "c9 b7")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (4, 4)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_reduce_bilinear(self, mode):
+ case = self.make_case(mode, (8, 8), 0xE1)
+ case = case.resize((4, 4), Image.Resampling.BILINEAR)
+ # fmt: off
+ data = ("e1 c9"
+ "c9 b7")
+ # fmt: on
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (4, 4)))
- def test_reduce_hamming(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (8, 8), 0xE1)
- case = case.resize((4, 4), Image.Resampling.HAMMING)
- # fmt: off
- data = ("e1 da"
- "da d3")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (4, 4)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_reduce_hamming(self, mode):
+ case = self.make_case(mode, (8, 8), 0xE1)
+ case = case.resize((4, 4), Image.Resampling.HAMMING)
+ # fmt: off
+ data = ("e1 da"
+ "da d3")
+ # fmt: on
+ for channel in case.split():
+ 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"]:
case = self.make_case(mode, (12, 12), 0xE1)
case = case.resize((6, 6), Image.Resampling.BICUBIC)
@@ -145,79 +146,79 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split():
self.check_case(channel, self.make_sample(data, (6, 6)))
- def test_reduce_lanczos(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (16, 16), 0xE1)
- case = case.resize((8, 8), Image.Resampling.LANCZOS)
- # fmt: off
- data = ("e1 e0 e4 d7"
- "e0 df e3 d6"
- "e4 e3 e7 da"
- "d7 d6 d9 ce")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (8, 8)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_reduce_lanczos(self, mode):
+ case = self.make_case(mode, (16, 16), 0xE1)
+ case = case.resize((8, 8), Image.Resampling.LANCZOS)
+ # fmt: off
+ data = ("e1 e0 e4 d7"
+ "e0 df e3 d6"
+ "e4 e3 e7 da"
+ "d7 d6 d9 ce")
+ # fmt: on
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (8, 8)))
- def test_enlarge_box(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (2, 2), 0xE1)
- case = case.resize((4, 4), Image.Resampling.BOX)
- # fmt: off
- data = ("e1 e1"
- "e1 e1")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (4, 4)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_enlarge_box(self, mode):
+ case = self.make_case(mode, (2, 2), 0xE1)
+ case = case.resize((4, 4), Image.Resampling.BOX)
+ # fmt: off
+ data = ("e1 e1"
+ "e1 e1")
+ # fmt: on
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (4, 4)))
- def test_enlarge_bilinear(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (2, 2), 0xE1)
- case = case.resize((4, 4), Image.Resampling.BILINEAR)
- # fmt: off
- data = ("e1 b0"
- "b0 98")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (4, 4)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_enlarge_bilinear(self, mode):
+ case = self.make_case(mode, (2, 2), 0xE1)
+ case = case.resize((4, 4), Image.Resampling.BILINEAR)
+ # fmt: off
+ data = ("e1 b0"
+ "b0 98")
+ # fmt: on
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (4, 4)))
- def test_enlarge_hamming(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (2, 2), 0xE1)
- case = case.resize((4, 4), Image.Resampling.HAMMING)
- # fmt: off
- data = ("e1 d2"
- "d2 c5")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (4, 4)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_enlarge_hamming(self, mode):
+ case = self.make_case(mode, (2, 2), 0xE1)
+ case = case.resize((4, 4), Image.Resampling.HAMMING)
+ # fmt: off
+ data = ("e1 d2"
+ "d2 c5")
+ # fmt: on
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (4, 4)))
- def test_enlarge_bicubic(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (4, 4), 0xE1)
- case = case.resize((8, 8), Image.Resampling.BICUBIC)
- # fmt: off
- data = ("e1 e5 ee b9"
- "e5 e9 f3 bc"
- "ee f3 fd c1"
- "b9 bc c1 a2")
- # fmt: on
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (8, 8)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_enlarge_bicubic(self, mode):
+ case = self.make_case(mode, (4, 4), 0xE1)
+ case = case.resize((8, 8), Image.Resampling.BICUBIC)
+ # fmt: off
+ data = ("e1 e5 ee b9"
+ "e5 e9 f3 bc"
+ "ee f3 fd c1"
+ "b9 bc c1 a2")
+ # fmt: on
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (8, 8)))
- def test_enlarge_lanczos(self):
- for mode in ["RGBX", "RGB", "La", "L"]:
- case = self.make_case(mode, (6, 6), 0xE1)
- case = case.resize((12, 12), Image.Resampling.LANCZOS)
- data = (
- "e1 e0 db ed f5 b8"
- "e0 df da ec f3 b7"
- "db db d6 e7 ee b5"
- "ed ec e6 fb ff bf"
- "f5 f4 ee ff ff c4"
- "b8 b7 b4 bf c4 a0"
- )
- for channel in case.split():
- self.check_case(channel, self.make_sample(data, (12, 12)))
+ @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
+ def test_enlarge_lanczos(self, mode):
+ case = self.make_case(mode, (6, 6), 0xE1)
+ case = case.resize((12, 12), Image.Resampling.LANCZOS)
+ data = (
+ "e1 e0 db ed f5 b8"
+ "e0 df da ec f3 b7"
+ "db db d6 e7 ee b5"
+ "ed ec e6 fb ff bf"
+ "f5 f4 ee ff ff c4"
+ "b8 b7 b4 bf c4 a0"
+ )
+ for channel in case.split():
+ self.check_case(channel, self.make_sample(data, (12, 12)))
def test_box_filter_correct_range(self):
im = Image.new("RGB", (8, 8), "#1688ff").resize(
@@ -419,40 +420,43 @@ class TestCoreResampleCoefficients:
class TestCoreResampleBox:
- def test_wrong_arguments(self):
- im = hopper()
- for resample in (
+ @pytest.mark.parametrize(
+ "resample",
+ (
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
- ):
- im.resize((32, 32), resample, (0, 0, im.width, im.height))
- im.resize((32, 32), resample, (20, 20, im.width, im.height))
- im.resize((32, 32), resample, (20, 20, 20, 100))
- im.resize((32, 32), resample, (20, 20, 100, 20))
+ ),
+ )
+ def test_wrong_arguments(self, resample):
+ im = hopper()
+ im.resize((32, 32), resample, (0, 0, im.width, im.height))
+ im.resize((32, 32), resample, (20, 20, im.width, im.height))
+ im.resize((32, 32), resample, (20, 20, 20, 100))
+ im.resize((32, 32), resample, (20, 20, 100, 20))
- with pytest.raises(TypeError, match="must be sequence of length 4"):
- im.resize((32, 32), resample, (im.width, im.height))
+ with pytest.raises(TypeError, match="must be sequence of length 4"):
+ im.resize((32, 32), resample, (im.width, im.height))
- with pytest.raises(ValueError, match="can't be negative"):
- im.resize((32, 32), resample, (-20, 20, 100, 100))
- with pytest.raises(ValueError, match="can't be negative"):
- im.resize((32, 32), resample, (20, -20, 100, 100))
+ with pytest.raises(ValueError, match="can't be negative"):
+ im.resize((32, 32), resample, (-20, 20, 100, 100))
+ with pytest.raises(ValueError, match="can't be negative"):
+ im.resize((32, 32), resample, (20, -20, 100, 100))
- with pytest.raises(ValueError, match="can't be empty"):
- im.resize((32, 32), resample, (20.1, 20, 20, 100))
- with pytest.raises(ValueError, match="can't be empty"):
- im.resize((32, 32), resample, (20, 20.1, 100, 20))
- with pytest.raises(ValueError, match="can't be empty"):
- im.resize((32, 32), resample, (20.1, 20.1, 20, 20))
+ with pytest.raises(ValueError, match="can't be empty"):
+ im.resize((32, 32), resample, (20.1, 20, 20, 100))
+ with pytest.raises(ValueError, match="can't be empty"):
+ im.resize((32, 32), resample, (20, 20.1, 100, 20))
+ with pytest.raises(ValueError, match="can't be empty"):
+ im.resize((32, 32), resample, (20.1, 20.1, 20, 20))
- with pytest.raises(ValueError, match="can't exceed"):
- im.resize((32, 32), resample, (0, 0, im.width + 1, im.height))
- with pytest.raises(ValueError, match="can't exceed"):
- im.resize((32, 32), resample, (0, 0, im.width, im.height + 1))
+ with pytest.raises(ValueError, match="can't exceed"):
+ im.resize((32, 32), resample, (0, 0, im.width + 1, im.height))
+ with pytest.raises(ValueError, match="can't exceed"):
+ im.resize((32, 32), resample, (0, 0, im.width, im.height + 1))
def resize_tiled(self, im, dst_size, xtiles, ytiles):
def split_range(size, tiles):
@@ -509,14 +513,16 @@ class TestCoreResampleBox:
with pytest.raises(AssertionError, match=r"difference 29\."):
assert_image_similar(reference, without_box, 5)
- def test_formats(self):
- for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]:
- for mode in ["RGB", "L", "RGBA", "LA", "I", ""]:
- im = hopper(mode)
- box = (20, 20, im.size[0] - 20, im.size[1] - 20)
- with_box = im.resize((32, 32), resample, box)
- cropped = im.crop(box).resize((32, 32), resample)
- assert_image_similar(cropped, with_box, 0.4)
+ @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", ""))
+ @pytest.mark.parametrize(
+ "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR)
+ )
+ def test_formats(self, mode, resample):
+ im = hopper(mode)
+ box = (20, 20, im.size[0] - 20, im.size[1] - 20)
+ with_box = im.resize((32, 32), resample, box)
+ cropped = im.crop(box).resize((32, 32), resample)
+ assert_image_similar(cropped, with_box, 0.4)
def test_passthrough(self):
# When no resize is required
@@ -548,44 +554,48 @@ class TestCoreResampleBox:
# check that the difference at least that much
assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}")
- def test_skip_horizontal(self):
+ @pytest.mark.parametrize(
+ "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
+ )
+ def test_skip_horizontal(self, flt):
# Can skip resize for one dimension
im = hopper()
- for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
- for size, box in [
- ((40, 50), (0, 0, 40, 90)),
- ((40, 50), (0, 20, 40, 90)),
- ((40, 50), (10, 0, 50, 90)),
- ((40, 50), (10, 20, 50, 90)),
- ]:
- res = im.resize(size, flt, box)
- assert res.size == size
- # Borders should be slightly different
- assert_image_similar(
- res,
- im.crop(box).resize(size, flt),
- 0.4,
- f">>> {size} {box} {flt}",
- )
+ for size, box in [
+ ((40, 50), (0, 0, 40, 90)),
+ ((40, 50), (0, 20, 40, 90)),
+ ((40, 50), (10, 0, 50, 90)),
+ ((40, 50), (10, 20, 50, 90)),
+ ]:
+ res = im.resize(size, flt, box)
+ assert res.size == size
+ # Borders should be slightly different
+ assert_image_similar(
+ res,
+ im.crop(box).resize(size, flt),
+ 0.4,
+ f">>> {size} {box} {flt}",
+ )
- def test_skip_vertical(self):
+ @pytest.mark.parametrize(
+ "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
+ )
+ def test_skip_vertical(self, flt):
# Can skip resize for one dimension
im = hopper()
- for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
- for size, box in [
- ((40, 50), (0, 0, 90, 50)),
- ((40, 50), (20, 0, 90, 50)),
- ((40, 50), (0, 10, 90, 60)),
- ((40, 50), (20, 10, 90, 60)),
- ]:
- res = im.resize(size, flt, box)
- assert res.size == size
- # Borders should be slightly different
- assert_image_similar(
- res,
- im.crop(box).resize(size, flt),
- 0.4,
- f">>> {size} {box} {flt}",
- )
+ for size, box in [
+ ((40, 50), (0, 0, 90, 50)),
+ ((40, 50), (20, 0, 90, 50)),
+ ((40, 50), (0, 10, 90, 60)),
+ ((40, 50), (20, 10, 90, 60)),
+ ]:
+ res = im.resize(size, flt, box)
+ assert res.size == size
+ # Borders should be slightly different
+ assert_image_similar(
+ res,
+ im.crop(box).resize(size, flt),
+ 0.4,
+ f">>> {size} {box} {flt}",
+ )
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index 8347fabb9..83c54cf62 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -22,24 +22,15 @@ class TestImagingCoreResize:
im.load()
return im._new(im.im.resize(size, f))
- def test_nearest_mode(self):
- for mode in [
- "1",
- "P",
- "L",
- "I",
- "F",
- "RGB",
- "RGBA",
- "CMYK",
- "YCbCr",
- "I;16",
- ]: # exotic mode
- im = hopper(mode)
- r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
- assert r.mode == mode
- assert r.size == (15, 12)
- assert r.im.bands == im.im.bands
+ @pytest.mark.parametrize(
+ "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16")
+ )
+ def test_nearest_mode(self, mode):
+ im = hopper(mode)
+ r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
+ assert r.mode == mode
+ assert r.size == (15, 12)
+ assert r.im.bands == im.im.bands
def test_convolution_modes(self):
with pytest.raises(ValueError):
@@ -55,33 +46,58 @@ class TestImagingCoreResize:
assert r.size == (15, 12)
assert r.im.bands == im.im.bands
- def test_reduce_filters(self):
- for f in [
+ @pytest.mark.parametrize(
+ "resample",
+ (
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
- ]:
- r = self.resize(hopper("RGB"), (15, 12), f)
- assert r.mode == "RGB"
- assert r.size == (15, 12)
+ ),
+ )
+ def test_reduce_filters(self, resample):
+ r = self.resize(hopper("RGB"), (15, 12), resample)
+ assert r.mode == "RGB"
+ assert r.size == (15, 12)
- def test_enlarge_filters(self):
- for f in [
+ @pytest.mark.parametrize(
+ "resample",
+ (
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
- ]:
- r = self.resize(hopper("RGB"), (212, 195), f)
- assert r.mode == "RGB"
- assert r.size == (212, 195)
+ ),
+ )
+ def test_enlarge_filters(self, resample):
+ r = self.resize(hopper("RGB"), (212, 195), resample)
+ assert r.mode == "RGB"
+ assert r.size == (212, 195)
- def test_endianness(self):
+ @pytest.mark.parametrize(
+ "resample",
+ (
+ Image.Resampling.NEAREST,
+ Image.Resampling.BOX,
+ Image.Resampling.BILINEAR,
+ Image.Resampling.HAMMING,
+ Image.Resampling.BICUBIC,
+ Image.Resampling.LANCZOS,
+ ),
+ )
+ @pytest.mark.parametrize(
+ "mode, channels_set",
+ (
+ ("RGB", ("blank", "filled", "dirty")),
+ ("RGBA", ("blank", "blank", "filled", "dirty")),
+ ("LA", ("filled", "dirty")),
+ ),
+ )
+ def test_endianness(self, resample, mode, channels_set):
# Make an image with one colored pixel, in one channel.
# When resized, that channel should be the same as a GS image.
# Other channels should be unaffected.
@@ -95,47 +111,37 @@ class TestImagingCoreResize:
}
samples["dirty"].putpixel((1, 1), 128)
- for f in [
+ # samples resized with current filter
+ references = {
+ name: self.resize(ch, (4, 4), resample) for name, ch in samples.items()
+ }
+
+ for channels in set(permutations(channels_set)):
+ # compile image from different channels permutations
+ im = Image.merge(mode, [samples[ch] for ch in channels])
+ resized = self.resize(im, (4, 4), resample)
+
+ for i, ch in enumerate(resized.split()):
+ # check what resized channel in image is the same
+ # as separately resized channel
+ assert_image_equal(ch, references[channels[i]])
+
+ @pytest.mark.parametrize(
+ "resample",
+ (
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
- ]:
- # samples resized with current filter
- references = {
- name: self.resize(ch, (4, 4), f) 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)):
- # compile image from different channels permutations
- im = Image.merge(mode, [samples[ch] for ch in channels])
- resized = self.resize(im, (4, 4), f)
-
- for i, ch in enumerate(resized.split()):
- # check what resized channel in image is the same
- # as separately resized channel
- assert_image_equal(ch, references[channels[i]])
-
- def test_enlarge_zero(self):
- for f in [
- Image.Resampling.NEAREST,
- Image.Resampling.BOX,
- Image.Resampling.BILINEAR,
- Image.Resampling.HAMMING,
- Image.Resampling.BICUBIC,
- Image.Resampling.LANCZOS,
- ]:
- r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f)
- assert r.mode == "RGB"
- assert r.size == (212, 195)
- assert r.getdata()[0] == (0, 0, 0)
+ ),
+ )
+ def test_enlarge_zero(self, resample):
+ r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
+ assert r.mode == "RGB"
+ assert r.size == (212, 195)
+ assert r.getdata()[0] == (0, 0, 0)
def test_unknown_filter(self):
with pytest.raises(ValueError):
@@ -179,74 +185,71 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99
)
- def test_reducing_gap_1(self, gradients_image):
- for box, epsilon in [
- (None, 4),
- ((1.1, 2.2, 510.8, 510.9), 4),
- ((3, 10, 410, 256), 10),
- ]:
- ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
- im = gradients_image.resize(
- (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
- )
-
- with pytest.raises(AssertionError):
- assert_image_equal(ref, im)
-
- assert_image_similar(ref, im, epsilon)
-
- def test_reducing_gap_2(self, gradients_image):
- for box, epsilon in [
- (None, 1.5),
- ((1.1, 2.2, 510.8, 510.9), 1.5),
- ((3, 10, 410, 256), 1),
- ]:
- ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
- im = gradients_image.resize(
- (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
- )
-
- with pytest.raises(AssertionError):
- assert_image_equal(ref, im)
-
- assert_image_similar(ref, im, epsilon)
-
- def test_reducing_gap_3(self, gradients_image):
- for box, epsilon in [
- (None, 1),
- ((1.1, 2.2, 510.8, 510.9), 1),
- ((3, 10, 410, 256), 0.5),
- ]:
- ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
- im = gradients_image.resize(
- (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
- )
-
- with pytest.raises(AssertionError):
- assert_image_equal(ref, im)
-
- assert_image_similar(ref, im, epsilon)
-
- def test_reducing_gap_8(self, gradients_image):
- for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]:
- ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
- im = gradients_image.resize(
- (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
- )
+ @pytest.mark.parametrize(
+ "box, epsilon",
+ ((None, 4), ((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)
+ im = gradients_image.resize(
+ (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
+ )
+ with pytest.raises(AssertionError):
assert_image_equal(ref, im)
- def test_box_filter(self, gradients_image):
- for box, epsilon in [
- ((0, 0, 512, 512), 5.5),
- ((0.9, 1.7, 128, 128), 9.5),
- ]:
- ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
- im = gradients_image.resize(
- (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
- )
+ assert_image_similar(ref, im, epsilon)
- assert_image_similar(ref, im, epsilon)
+ @pytest.mark.parametrize(
+ "box, epsilon",
+ ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)),
+ )
+ def test_reducing_gap_2(self, gradients_image, box, epsilon):
+ ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
+ im = gradients_image.resize(
+ (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
+ )
+
+ with pytest.raises(AssertionError):
+ assert_image_equal(ref, im)
+
+ assert_image_similar(ref, im, epsilon)
+
+ @pytest.mark.parametrize(
+ "box, epsilon",
+ ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)),
+ )
+ def test_reducing_gap_3(self, gradients_image, box, epsilon):
+ ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
+ im = gradients_image.resize(
+ (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
+ )
+
+ with pytest.raises(AssertionError):
+ assert_image_equal(ref, im)
+
+ assert_image_similar(ref, im, epsilon)
+
+ @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)))
+ def test_reducing_gap_8(self, gradients_image, box):
+ ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
+ im = gradients_image.resize(
+ (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
+ )
+
+ assert_image_equal(ref, im)
+
+ @pytest.mark.parametrize(
+ "box, epsilon",
+ (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)),
+ )
+ def test_box_filter(self, gradients_image, box, epsilon):
+ ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
+ im = gradients_image.resize(
+ (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
+ )
+
+ assert_image_similar(ref, im, epsilon)
class TestImageResize:
@@ -273,15 +276,14 @@ class TestImageResize:
im = im.resize((64, 64))
assert im.size == (64, 64)
- def test_default_filter(self):
- for mode in "L", "RGB", "I", "F":
- im = hopper(mode)
- assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
+ @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F"))
+ def test_default_filter_bicubic(self, mode):
+ im = hopper(mode)
+ assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
- for mode in "1", "P":
- im = hopper(mode)
- assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
-
- for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16":
- im = hopper(mode)
- assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
+ @pytest.mark.parametrize(
+ "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16")
+ )
+ def test_default_filter_nearest(self, mode):
+ im = hopper(mode)
+ assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py
index f96864c53..a19f19831 100644
--- a/Tests/test_image_rotate.py
+++ b/Tests/test_image_rotate.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image
from .helper import (
@@ -22,26 +24,26 @@ def rotate(im, mode, angle, center=None, translate=None):
assert out.size != im.size
-def test_mode():
- for mode in ("1", "P", "L", "RGB", "I", "F"):
- im = hopper(mode)
- rotate(im, mode, 45)
+@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
+def test_mode(mode):
+ im = hopper(mode)
+ rotate(im, mode, 45)
-def test_angle():
- for angle in (0, 90, 180, 270):
- with Image.open("Tests/images/test-card.png") as im:
- rotate(im, im.mode, angle)
-
- im = hopper()
- assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
-
-
-def test_zero():
- for angle in (0, 45, 90, 180, 270):
- im = Image.new("RGB", (0, 0))
+@pytest.mark.parametrize("angle", (0, 90, 180, 270))
+def test_angle(angle):
+ with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
+ im = hopper()
+ assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
+
+
+@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
+def test_zero(angle):
+ im = Image.new("RGB", (0, 0))
+ rotate(im, im.mode, angle)
+
def test_resample():
# Target image creation, inspected by eye.
diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py
index fbed276b8..5cb7c9a8b 100644
--- a/Tests/test_image_split.py
+++ b/Tests/test_image_split.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image, features
from .helper import assert_image_equal, hopper
@@ -29,19 +31,12 @@ def test_split():
assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)]
-def test_split_merge():
- def split_merge(mode):
- return Image.merge(mode, hopper(mode).split())
-
- assert_image_equal(hopper("1"), split_merge("1"))
- assert_image_equal(hopper("L"), split_merge("L"))
- assert_image_equal(hopper("I"), split_merge("I"))
- assert_image_equal(hopper("F"), split_merge("F"))
- assert_image_equal(hopper("P"), split_merge("P"))
- assert_image_equal(hopper("RGB"), split_merge("RGB"))
- assert_image_equal(hopper("RGBA"), split_merge("RGBA"))
- assert_image_equal(hopper("CMYK"), split_merge("CMYK"))
- assert_image_equal(hopper("YCbCr"), split_merge("YCbCr"))
+@pytest.mark.parametrize(
+ "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr")
+)
+def test_split_merge(mode):
+ expected = Image.merge(mode, hopper(mode).split())
+ assert_image_equal(hopper(mode), expected)
def test_split_open(tmp_path):
diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py
index 20cc101ed..4fd07a2b4 100644
--- a/Tests/test_image_thumbnail.py
+++ b/Tests/test_image_thumbnail.py
@@ -97,6 +97,28 @@ def test_load_first():
im.thumbnail((64, 64))
assert im.size == (64, 10)
+ # Test thumbnail(), without draft(),
+ # on an image that is large enough once load() has changed the size
+ with Image.open("Tests/images/g4_orientation_5.tif") as im:
+ im.thumbnail((590, 88), reducing_gap=None)
+ assert im.size == (590, 88)
+
+
+def test_load_first_unless_jpeg():
+ # Test that thumbnail() still uses draft() for JPEG
+ with Image.open("Tests/images/hopper.jpg") as im:
+ draft = im.draft
+
+ def im_draft(mode, size):
+ result = draft(mode, size)
+ assert result is not None
+
+ return result
+
+ im.draft = im_draft
+
+ im.thumbnail((64, 64))
+
# valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing")
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index ac0e74969..a78349801 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -75,23 +75,25 @@ class TestImageTransform:
assert_image_equal(transformed, scaled)
- def test_fill(self):
- for mode, pixel in [
- ["RGB", (255, 0, 0)],
- ["RGBA", (255, 0, 0, 255)],
- ["LA", (76, 0)],
- ]:
- im = hopper(mode)
- (w, h) = im.size
- transformed = im.transform(
- im.size,
- Image.Transform.EXTENT,
- (0, 0, w * 2, h * 2),
- Image.Resampling.BILINEAR,
- fillcolor="red",
- )
-
- assert transformed.getpixel((w - 1, h - 1)) == pixel
+ @pytest.mark.parametrize(
+ "mode, expected_pixel",
+ (
+ ("RGB", (255, 0, 0)),
+ ("RGBA", (255, 0, 0, 255)),
+ ("LA", (76, 0)),
+ ),
+ )
+ def test_fill(self, mode, expected_pixel):
+ im = hopper(mode)
+ (w, h) = im.size
+ transformed = im.transform(
+ im.size,
+ Image.Transform.EXTENT,
+ (0, 0, w * 2, h * 2),
+ Image.Resampling.BILINEAR,
+ fillcolor="red",
+ )
+ assert transformed.getpixel((w - 1, h - 1)) == expected_pixel
def test_mesh(self):
# this should be a checkerboard of halfsized hoppers in ul, lr
@@ -222,14 +224,12 @@ class TestImageTransform:
with pytest.raises(ValueError):
im.transform((100, 100), None)
- def test_unknown_resampling_filter(self):
+ @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
+ def test_unknown_resampling_filter(self, resample):
with hopper() as im:
(w, h) = im.size
- for resample in (Image.Resampling.BOX, "unknown"):
- with pytest.raises(ValueError):
- im.transform(
- (100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample
- )
+ with pytest.raises(ValueError):
+ im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample)
class TestImageTransformAffine:
@@ -239,7 +239,16 @@ class TestImageTransformAffine:
im = hopper("RGB")
return im.crop((10, 20, im.width - 10, im.height - 20))
- def _test_rotate(self, deg, transpose):
+ @pytest.mark.parametrize(
+ "deg, transpose",
+ (
+ (0, None),
+ (90, Image.Transpose.ROTATE_90),
+ (180, Image.Transpose.ROTATE_180),
+ (270, Image.Transpose.ROTATE_270),
+ ),
+ )
+ def test_rotate(self, deg, transpose):
im = self._test_image()
angle = -math.radians(deg)
@@ -271,77 +280,65 @@ class TestImageTransformAffine:
)
assert_image_equal(transposed, transformed)
- def test_rotate_0_deg(self):
- self._test_rotate(0, None)
-
- def test_rotate_90_deg(self):
- self._test_rotate(90, Image.Transpose.ROTATE_90)
-
- def test_rotate_180_deg(self):
- self._test_rotate(180, Image.Transpose.ROTATE_180)
-
- def test_rotate_270_deg(self):
- self._test_rotate(270, Image.Transpose.ROTATE_270)
-
- def _test_resize(self, scale, epsilonscale):
+ @pytest.mark.parametrize(
+ "scale, epsilon_scale",
+ (
+ (1.1, 6.9),
+ (1.5, 5.5),
+ (2.0, 5.5),
+ (2.3, 3.7),
+ (2.5, 3.7),
+ ),
+ )
+ @pytest.mark.parametrize(
+ "resample,epsilon",
+ (
+ (Image.Resampling.NEAREST, 0),
+ (Image.Resampling.BILINEAR, 2),
+ (Image.Resampling.BICUBIC, 1),
+ ),
+ )
+ def test_resize(self, scale, epsilon_scale, resample, epsilon):
im = self._test_image()
size_up = int(round(im.width * scale)), int(round(im.height * scale))
matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0]
matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0]
- for resample, epsilon in [
+ transformed = im.transform(size_up, self.transform, matrix_up, resample)
+ transformed = transformed.transform(
+ im.size, self.transform, matrix_down, resample
+ )
+ assert_image_similar(transformed, im, epsilon * epsilon_scale)
+
+ @pytest.mark.parametrize(
+ "x, y, epsilon_scale",
+ (
+ (0.1, 0, 3.7),
+ (0.6, 0, 9.1),
+ (50, 50, 0),
+ ),
+ )
+ @pytest.mark.parametrize(
+ "resample, epsilon",
+ (
(Image.Resampling.NEAREST, 0),
- (Image.Resampling.BILINEAR, 2),
+ (Image.Resampling.BILINEAR, 1.5),
(Image.Resampling.BICUBIC, 1),
- ]:
- transformed = im.transform(size_up, self.transform, matrix_up, resample)
- transformed = transformed.transform(
- im.size, self.transform, matrix_down, resample
- )
- assert_image_similar(transformed, im, epsilon * epsilonscale)
-
- def test_resize_1_1x(self):
- self._test_resize(1.1, 6.9)
-
- def test_resize_1_5x(self):
- self._test_resize(1.5, 5.5)
-
- def test_resize_2_0x(self):
- self._test_resize(2.0, 5.5)
-
- def test_resize_2_3x(self):
- self._test_resize(2.3, 3.7)
-
- def test_resize_2_5x(self):
- self._test_resize(2.5, 3.7)
-
- def _test_translate(self, x, y, epsilonscale):
+ ),
+ )
+ def test_translate(self, x, y, epsilon_scale, resample, epsilon):
im = self._test_image()
size_up = int(round(im.width + x)), int(round(im.height + y))
matrix_up = [1, 0, -x, 0, 1, -y, 0, 0]
matrix_down = [1, 0, x, 0, 1, y, 0, 0]
- for resample, epsilon in [
- (Image.Resampling.NEAREST, 0),
- (Image.Resampling.BILINEAR, 1.5),
- (Image.Resampling.BICUBIC, 1),
- ]:
- transformed = im.transform(size_up, self.transform, matrix_up, resample)
- transformed = transformed.transform(
- im.size, self.transform, matrix_down, resample
- )
- assert_image_similar(transformed, im, epsilon * epsilonscale)
-
- def test_translate_0_1(self):
- self._test_translate(0.1, 0, 3.7)
-
- def test_translate_0_6(self):
- self._test_translate(0.6, 0, 9.1)
-
- def test_translate_50(self):
- self._test_translate(50, 50, 0)
+ transformed = im.transform(size_up, self.transform, matrix_up, resample)
+ transformed = transformed.transform(
+ im.size, self.transform, matrix_down, resample
+ )
+ assert_image_similar(transformed, im, epsilon * epsilon_scale)
class TestImageTransformPerspective(TestImageTransformAffine):
diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py
index 6408e1564..877f439ca 100644
--- a/Tests/test_image_transpose.py
+++ b/Tests/test_image_transpose.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL.Image import Transpose
from . import helper
@@ -9,157 +11,136 @@ HOPPER = {
}
-def test_flip_left_right():
- def transpose(mode):
- im = HOPPER[mode]
- out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
- assert out.mode == mode
- assert out.size == im.size
+@pytest.mark.parametrize("mode", HOPPER)
+def test_flip_left_right(mode):
+ im = HOPPER[mode]
+ out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
+ assert out.mode == mode
+ assert out.size == im.size
- x, y = im.size
- assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1))
- assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
- assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2))
- assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2))
-
- for mode in HOPPER:
- transpose(mode)
+ x, y = im.size
+ assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1))
+ assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
+ assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2))
+ assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2))
-def test_flip_top_bottom():
- def transpose(mode):
- im = HOPPER[mode]
- out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
- assert out.mode == mode
- assert out.size == im.size
+@pytest.mark.parametrize("mode", HOPPER)
+def test_flip_top_bottom(mode):
+ im = HOPPER[mode]
+ out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
+ assert out.mode == mode
+ assert out.size == im.size
- x, y = im.size
- assert im.getpixel((1, 1)) == out.getpixel((1, y - 2))
- assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2))
- assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
- assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1))
-
- for mode in HOPPER:
- transpose(mode)
+ x, y = im.size
+ assert im.getpixel((1, 1)) == out.getpixel((1, y - 2))
+ assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2))
+ assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
+ assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1))
-def test_rotate_90():
- def transpose(mode):
- im = HOPPER[mode]
- out = im.transpose(Transpose.ROTATE_90)
- assert out.mode == mode
- assert out.size == im.size[::-1]
+@pytest.mark.parametrize("mode", HOPPER)
+def test_rotate_90(mode):
+ im = HOPPER[mode]
+ out = im.transpose(Transpose.ROTATE_90)
+ assert out.mode == mode
+ assert out.size == im.size[::-1]
- x, y = im.size
- assert im.getpixel((1, 1)) == out.getpixel((1, x - 2))
- assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
- assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2))
- assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1))
-
- for mode in HOPPER:
- transpose(mode)
+ x, y = im.size
+ assert im.getpixel((1, 1)) == out.getpixel((1, x - 2))
+ assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1))
+ assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2))
+ assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1))
-def test_rotate_180():
- def transpose(mode):
- im = HOPPER[mode]
- out = im.transpose(Transpose.ROTATE_180)
- assert out.mode == mode
- assert out.size == im.size
+@pytest.mark.parametrize("mode", HOPPER)
+def test_rotate_180(mode):
+ im = HOPPER[mode]
+ out = im.transpose(Transpose.ROTATE_180)
+ assert out.mode == mode
+ assert out.size == im.size
- x, y = im.size
- assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2))
- assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2))
- assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1))
- assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
-
- for mode in HOPPER:
- transpose(mode)
+ x, y = im.size
+ assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2))
+ assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2))
+ assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1))
+ assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
-def test_rotate_270():
- def transpose(mode):
- im = HOPPER[mode]
- out = im.transpose(Transpose.ROTATE_270)
- assert out.mode == mode
- assert out.size == im.size[::-1]
+@pytest.mark.parametrize("mode", HOPPER)
+def test_rotate_270(mode):
+ im = HOPPER[mode]
+ out = im.transpose(Transpose.ROTATE_270)
+ assert out.mode == mode
+ assert out.size == im.size[::-1]
- x, y = im.size
- assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1))
- assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2))
- assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
- assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2))
-
- for mode in HOPPER:
- transpose(mode)
+ x, y = im.size
+ assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1))
+ assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2))
+ assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
+ assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2))
-def test_transpose():
- def transpose(mode):
- im = HOPPER[mode]
- out = im.transpose(Transpose.TRANSPOSE)
- assert out.mode == mode
- assert out.size == im.size[::-1]
+@pytest.mark.parametrize("mode", HOPPER)
+def test_transpose(mode):
+ im = HOPPER[mode]
+ out = im.transpose(Transpose.TRANSPOSE)
+ assert out.mode == mode
+ assert out.size == im.size[::-1]
- x, y = im.size
- assert im.getpixel((1, 1)) == out.getpixel((1, 1))
- assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2))
- assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1))
- assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2))
-
- for mode in HOPPER:
- transpose(mode)
+ x, y = im.size
+ assert im.getpixel((1, 1)) == out.getpixel((1, 1))
+ assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2))
+ assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1))
+ assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2))
-def test_tranverse():
- def transpose(mode):
- im = HOPPER[mode]
- out = im.transpose(Transpose.TRANSVERSE)
- assert out.mode == mode
- assert out.size == im.size[::-1]
+@pytest.mark.parametrize("mode", HOPPER)
+def test_tranverse(mode):
+ im = HOPPER[mode]
+ out = im.transpose(Transpose.TRANSVERSE)
+ assert out.mode == mode
+ assert out.size == im.size[::-1]
- x, y = im.size
- assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2))
- assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1))
- assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2))
- assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
-
- for mode in HOPPER:
- transpose(mode)
+ x, y = im.size
+ assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2))
+ assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1))
+ assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2))
+ assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
-def test_roundtrip():
- for mode in HOPPER:
- im = HOPPER[mode]
+@pytest.mark.parametrize("mode", HOPPER)
+def test_roundtrip(mode):
+ im = HOPPER[mode]
- def transpose(first, second):
- return im.transpose(first).transpose(second)
+ def transpose(first, second):
+ return im.transpose(first).transpose(second)
- assert_image_equal(
- im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT)
- )
- assert_image_equal(
- im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM)
- )
- assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270))
- assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180))
- assert_image_equal(
- im.transpose(Transpose.TRANSPOSE),
- transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM),
- )
- assert_image_equal(
- im.transpose(Transpose.TRANSPOSE),
- transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT),
- )
- assert_image_equal(
- im.transpose(Transpose.TRANSVERSE),
- transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT),
- )
- assert_image_equal(
- im.transpose(Transpose.TRANSVERSE),
- transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM),
- )
- assert_image_equal(
- im.transpose(Transpose.TRANSVERSE),
- transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE),
- )
+ assert_image_equal(
+ im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT)
+ )
+ assert_image_equal(
+ im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM)
+ )
+ assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270))
+ assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180))
+ assert_image_equal(
+ im.transpose(Transpose.TRANSPOSE),
+ transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM),
+ )
+ assert_image_equal(
+ im.transpose(Transpose.TRANSPOSE),
+ transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT),
+ )
+ assert_image_equal(
+ im.transpose(Transpose.TRANSVERSE),
+ transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT),
+ )
+ assert_image_equal(
+ im.transpose(Transpose.TRANSVERSE),
+ transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM),
+ )
+ assert_image_equal(
+ im.transpose(Transpose.TRANSVERSE),
+ transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE),
+ )
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index 66a72a90e..3d8dbe6bb 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -174,19 +174,24 @@ def test_exceptions():
psRGB = ImageCms.createProfile("sRGB")
pLab = ImageCms.createProfile("LAB")
t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB")
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match="mode mismatch"):
t.apply_in_place(hopper("RGBA"))
# the procedural pyCMS API uses PyCMSError for all sorts of errors
with hopper() as im:
- with pytest.raises(ImageCms.PyCMSError):
+ with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"):
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")
- with pytest.raises(ImageCms.PyCMSError):
+
+ with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"):
ImageCms.getProfileName(None)
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)
@@ -201,15 +206,32 @@ def test_lab_color_profile():
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")
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")
+@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():
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
"""
- with pytest.raises(TypeError):
+ with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(0).tobytes()
- with pytest.raises(TypeError):
+ with pytest.raises(TypeError, match="Invalid type for Profile"):
ImageCms.ImageCmsProfile(1).tobytes()
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 69d1ac9fa..76b7c65cc 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -64,7 +64,9 @@ def test_mode_mismatch():
ImageDraw.ImageDraw(im, mode="L")
-def helper_arc(bbox, start, end):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
+def test_arc(bbox, start, end):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -76,16 +78,6 @@ def helper_arc(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
-def test_arc1():
- helper_arc(BBOX1, 0, 180)
- helper_arc(BBOX1, 0.5, 180.4)
-
-
-def test_arc2():
- helper_arc(BBOX2, 0, 180)
- helper_arc(BBOX2, 0.5, 180.4)
-
-
def test_arc_end_le_start():
# Arrange
im = Image.new("RGB", (W, H))
@@ -192,29 +184,21 @@ def test_bitmap():
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png")
-def helper_chord(mode, bbox, start, end):
+@pytest.mark.parametrize("mode", ("RGB", "L"))
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_chord(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
expected = f"Tests/images/imagedraw_chord_{mode}.png"
# Act
- draw.chord(bbox, start, end, fill="red", outline="yellow")
+ draw.chord(bbox, 0, 180, fill="red", outline="yellow")
# Assert
assert_image_similar_tofile(im, expected, 1)
-def test_chord1():
- for mode in ["RGB", "L"]:
- helper_chord(mode, BBOX1, 0, 180)
-
-
-def test_chord2():
- for mode in ["RGB", "L"]:
- helper_chord(mode, BBOX2, 0, 180)
-
-
def test_chord_width():
# Arrange
im = Image.new("RGB", (W, H))
@@ -263,7 +247,9 @@ def test_chord_too_fat():
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png")
-def helper_ellipse(mode, bbox):
+@pytest.mark.parametrize("mode", ("RGB", "L"))
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_ellipse(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@@ -276,16 +262,6 @@ def helper_ellipse(mode, bbox):
assert_image_similar_tofile(im, expected, 1)
-def test_ellipse1():
- for mode in ["RGB", "L"]:
- helper_ellipse(mode, BBOX1)
-
-
-def test_ellipse2():
- for mode in ["RGB", "L"]:
- helper_ellipse(mode, BBOX2)
-
-
def test_ellipse_translucent():
# Arrange
im = Image.new("RGB", (W, H))
@@ -405,7 +381,8 @@ def test_ellipse_various_sizes_filled():
)
-def helper_line(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -417,14 +394,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def test_line1():
- helper_line(POINTS1)
-
-
-def test_line2():
- helper_line(POINTS2)
-
-
def test_shape1():
# Arrange
im = Image.new("RGB", (100, 100), "white")
@@ -484,7 +453,9 @@ def test_transform():
assert_image_equal(im, expected)
-def helper_pieslice(bbox, start, end):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
+def test_pieslice(bbox, start, end):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -496,16 +467,6 @@ def helper_pieslice(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
-def test_pieslice1():
- helper_pieslice(BBOX1, -92, 46)
- helper_pieslice(BBOX1, -92.2, 46.2)
-
-
-def test_pieslice2():
- helper_pieslice(BBOX2, -92, 46)
- helper_pieslice(BBOX2, -92.2, 46.2)
-
-
def test_pieslice_width():
# Arrange
im = Image.new("RGB", (W, H))
@@ -585,7 +546,8 @@ def test_pieslice_no_spikes():
assert_image_equal(im, im_pre_erase)
-def helper_point(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_point(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -597,15 +559,8 @@ def helper_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
-def test_point1():
- helper_point(POINTS1)
-
-
-def test_point2():
- helper_point(POINTS2)
-
-
-def helper_polygon(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -617,28 +572,20 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
-def test_polygon1():
- helper_polygon(POINTS1)
-
-
-def test_polygon2():
- 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
# vertical (dx==0) and horizontal (dy==0) lines
- for mode in ["RGB", "L"]:
- # Arrange
- im = Image.new(mode, (W, H))
- draw = ImageDraw.Draw(im)
- expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png"
+ # Arrange
+ im = Image.new(mode, (W, H))
+ draw = ImageDraw.Draw(im)
+ expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png"
- # Act
- draw.polygon(KITE_POINTS, fill="blue", outline="yellow")
+ # Act
+ draw.polygon(KITE_POINTS, fill="blue", outline="yellow")
- # Assert
- assert_image_equal_tofile(im, expected)
+ # Assert
+ assert_image_equal_tofile(im, expected)
def test_polygon_1px_high():
@@ -682,7 +629,8 @@ def test_polygon_translucent():
assert_image_equal_tofile(im, expected)
-def helper_rectangle(bbox):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -694,14 +642,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
-def test_rectangle1():
- helper_rectangle(BBOX1)
-
-
-def test_rectangle2():
- helper_rectangle(BBOX2)
-
-
def test_big_rectangle():
# Test drawing a rectangle bigger than the image
# Arrange
@@ -1232,21 +1172,39 @@ def test_textsize_empty_string():
# Act
# Should not cause 'SystemError: returned NULL without setting an error'
- draw.textsize("")
- draw.textsize("\n")
- draw.textsize("test\n")
+ draw.textbbox((0, 0), "")
+ draw.textbbox((0, 0), "\n")
+ draw.textbbox((0, 0), "test\n")
+ draw.textlength("")
@skip_unless_feature("freetype2")
-def test_textsize_stroke():
+def test_textbbox_stroke():
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
# Act / Assert
- assert draw.textsize("A", font, stroke_width=2) == (16, 20)
- assert draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) == (52, 44)
+ assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20)
+ 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")
@@ -1296,6 +1254,23 @@ def test_stroke_multiline():
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():
# Prepare shape
x0, y0 = 5, 5
@@ -1468,7 +1443,7 @@ def test_discontiguous_corners_polygon():
assert_image_similar_tofile(img, expected, 1)
-def test_polygon():
+def test_polygon2():
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py
index 3a70176ce..6fc829f1a 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -1,5 +1,7 @@
import os.path
+import pytest
+
from PIL import Image, ImageDraw, ImageDraw2
from .helper import (
@@ -50,27 +52,19 @@ def test_sanity():
draw.line(list(range(10)), pen)
-def helper_ellipse(mode, bbox):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_ellipse(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("blue", width=2)
brush = ImageDraw2.Brush("green")
- expected = f"Tests/images/imagedraw_ellipse_{mode}.png"
# Act
draw.ellipse(bbox, pen, brush)
# Assert
- assert_image_similar_tofile(im, expected, 1)
-
-
-def test_ellipse1():
- helper_ellipse("RGB", BBOX1)
-
-
-def test_ellipse2():
- helper_ellipse("RGB", BBOX2)
+ assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1)
def test_ellipse_edge():
@@ -86,7 +80,8 @@ def test_ellipse_edge():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1)
-def helper_line(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -99,14 +94,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def test_line1_pen():
- helper_line(POINTS1)
-
-
-def test_line2_pen():
- helper_line(POINTS2)
-
-
def test_line_pen_as_brush():
# Arrange
im = Image.new("RGB", (W, H))
@@ -122,7 +109,8 @@ def test_line_pen_as_brush():
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def helper_polygon(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -136,15 +124,8 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
-def test_polygon1():
- helper_polygon(POINTS1)
-
-
-def test_polygon2():
- helper_polygon(POINTS2)
-
-
-def helper_rectangle(bbox):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -158,14 +139,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
-def test_rectangle1():
- helper_rectangle(BBOX1)
-
-
-def test_rectangle2():
- helper_rectangle(BBOX2)
-
-
def test_big_rectangle():
# Test drawing a rectangle bigger than the image
# Arrange
@@ -205,7 +178,9 @@ def test_textsize():
font = ImageDraw2.Font("white", FONT_PATH)
# Act
- size = draw.textsize("ImageDraw2", font)
+ with pytest.warns(DeprecationWarning) as log:
+ size = draw.textsize("ImageDraw2", font)
+ assert len(log) == 1
# Assert
assert size[1] == 12
@@ -221,9 +196,10 @@ def test_textsize_empty_string():
# Act
# Should not cause 'SystemError: returned NULL without setting an error'
- draw.textsize("", font)
- draw.textsize("\n", font)
- draw.textsize("test\n", font)
+ draw.textbbox((0, 0), "", font)
+ draw.textbbox((0, 0), "\n", font)
+ draw.textbbox((0, 0), "test\n", font)
+ draw.textlength("", font)
@skip_unless_feature("freetype2")
diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py
index 8bc94401e..221ef8cdb 100644
--- a/Tests/test_imageenhance.py
+++ b/Tests/test_imageenhance.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image, ImageEnhance
from .helper import assert_image_equal, hopper
@@ -39,17 +41,17 @@ def _check_alpha(im, original, op, amount):
)
-def test_alpha():
+@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
+def test_alpha(op):
# Issue https://github.com/python-pillow/Pillow/issues/899
# Is alpha preserved through image enhancement?
original = _half_transparent_image()
- for op in ["Color", "Brightness", "Contrast", "Sharpness"]:
- for amount in [0, 0.5, 1.0]:
- _check_alpha(
- getattr(ImageEnhance, op)(original).enhance(amount),
- original,
- op,
- amount,
- )
+ for amount in [0, 0.5, 1.0]:
+ _check_alpha(
+ getattr(ImageEnhance, op)(original).enhance(amount),
+ original,
+ op,
+ amount,
+ )
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 0c50303f9..a374e24c5 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -28,436 +28,527 @@ TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward"
pytestmark = skip_unless_feature("freetype2")
-class TestImageFont:
- LAYOUT_ENGINE = ImageFont.Layout.BASIC
+def test_sanity():
+ assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2"))
- def get_font(self):
- return ImageFont.truetype(
- FONT_PATH, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
- )
- def test_sanity(self):
- assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2"))
+@pytest.fixture(
+ scope="module",
+ params=[
+ pytest.param(ImageFont.Layout.BASIC),
+ pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
+ ],
+)
+def layout_engine(request):
+ return request.param
- def test_font_properties(self):
- ttf = self.get_font()
- assert ttf.path == FONT_PATH
- assert ttf.size == FONT_SIZE
- ttf_copy = ttf.font_variant()
- assert ttf_copy.path == FONT_PATH
- assert ttf_copy.size == FONT_SIZE
+@pytest.fixture(scope="module")
+def font(layout_engine):
+ return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine)
- ttf_copy = ttf.font_variant(size=FONT_SIZE + 1)
- assert ttf_copy.size == FONT_SIZE + 1
- second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
- ttf_copy = ttf.font_variant(font=second_font_path)
- assert ttf_copy.path == second_font_path
+def test_font_properties(font):
+ assert font.path == FONT_PATH
+ assert font.size == FONT_SIZE
- def test_font_with_name(self):
- self.get_font()
- self._render(FONT_PATH)
+ font_copy = font.font_variant()
+ assert font_copy.path == FONT_PATH
+ assert font_copy.size == FONT_SIZE
- def _font_as_bytes(self):
+ font_copy = font.font_variant(size=FONT_SIZE + 1)
+ assert font_copy.size == FONT_SIZE + 1
+
+ second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
+ font_copy = font.font_variant(font=second_font_path)
+ assert font_copy.path == second_font_path
+
+
+def _render(font, layout_engine):
+ txt = "Hello World!"
+ ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine)
+ ttf.getbbox(txt)
+
+ img = Image.new("RGB", (256, 64), "white")
+ d = ImageDraw.Draw(img)
+ d.text((10, 10), txt, font=ttf, fill="black")
+
+ return img
+
+
+def test_font_with_name(layout_engine):
+ _render(FONT_PATH, layout_engine)
+
+
+def test_font_with_filelike(layout_engine):
+ def _font_as_bytes():
with open(FONT_PATH, "rb") as f:
font_bytes = BytesIO(f.read())
return font_bytes
- def test_font_with_filelike(self):
- ttf = ImageFont.truetype(
- self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
- )
- ttf_copy = ttf.font_variant()
- assert ttf_copy.font_bytes == ttf.font_bytes
+ ttf = ImageFont.truetype(_font_as_bytes(), FONT_SIZE, layout_engine=layout_engine)
+ ttf_copy = ttf.font_variant()
+ assert ttf_copy.font_bytes == ttf.font_bytes
- self._render(self._font_as_bytes())
- # Usage note: making two fonts from the same buffer fails.
- # shared_bytes = self._font_as_bytes()
- # self._render(shared_bytes)
- # with pytest.raises(Exception):
- # _render(shared_bytes)
+ _render(_font_as_bytes(), layout_engine)
+ # Usage note: making two fonts from the same buffer fails.
+ # shared_bytes = _font_as_bytes()
+ # _render(shared_bytes)
+ # with pytest.raises(Exception):
+ # _render(shared_bytes)
- def test_font_with_open_file(self):
- with open(FONT_PATH, "rb") as f:
- self._render(f)
- def test_non_ascii_path(self, tmp_path):
- tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
- try:
- shutil.copy(FONT_PATH, tempfile)
- except UnicodeEncodeError:
- pytest.skip("Non-ASCII path could not be created")
+def test_font_with_open_file(layout_engine):
+ with open(FONT_PATH, "rb") as f:
+ _render(f, layout_engine)
- ImageFont.truetype(tempfile, FONT_SIZE)
- def _render(self, font):
- txt = "Hello World!"
- ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE)
- ttf.getsize(txt)
+def test_render_equal(layout_engine):
+ img_path = _render(FONT_PATH, layout_engine)
+ with open(FONT_PATH, "rb") as f:
+ font_filelike = BytesIO(f.read())
+ img_filelike = _render(font_filelike, layout_engine)
- img = Image.new("RGB", (256, 64), "white")
- d = ImageDraw.Draw(img)
- d.text((10, 10), txt, font=ttf, fill="black")
+ assert_image_equal(img_path, img_filelike)
- return img
- def test_render_equal(self):
- img_path = self._render(FONT_PATH)
- with open(FONT_PATH, "rb") as f:
- font_filelike = BytesIO(f.read())
- img_filelike = self._render(font_filelike)
+def test_non_ascii_path(tmp_path, layout_engine):
+ tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
+ try:
+ shutil.copy(FONT_PATH, tempfile)
+ except UnicodeEncodeError:
+ pytest.skip("Non-ASCII path could not be created")
- assert_image_equal(img_path, img_filelike)
+ ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine)
- def test_transparent_background(self):
- im = Image.new(mode="RGBA", size=(300, 100))
- draw = ImageDraw.Draw(im)
- ttf = self.get_font()
- txt = "Hello World!"
- draw.text((10, 10), txt, font=ttf)
+def test_transparent_background(font):
+ im = Image.new(mode="RGBA", size=(300, 100))
+ draw = ImageDraw.Draw(im)
- target = "Tests/images/transparent_background_text.png"
- assert_image_similar_tofile(im, target, 4.09)
+ txt = "Hello World!"
+ draw.text((10, 10), txt, font=font)
- target = "Tests/images/transparent_background_text_L.png"
- assert_image_similar_tofile(im.convert("L"), target, 0.01)
+ target = "Tests/images/transparent_background_text.png"
+ assert_image_similar_tofile(im, target, 4.09)
- def test_I16(self):
- im = Image.new(mode="I;16", size=(300, 100))
- draw = ImageDraw.Draw(im)
- ttf = self.get_font()
+ target = "Tests/images/transparent_background_text_L.png"
+ assert_image_similar_tofile(im.convert("L"), target, 0.01)
- txt = "Hello World!"
- draw.text((10, 10), txt, font=ttf)
- target = "Tests/images/transparent_background_text_L.png"
- assert_image_similar_tofile(im.convert("L"), target, 0.01)
+def test_I16(font):
+ im = Image.new(mode="I;16", size=(300, 100))
+ draw = ImageDraw.Draw(im)
- def test_textsize_equal(self):
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
- ttf = self.get_font()
+ txt = "Hello World!"
+ draw.text((10, 10), txt, font=font)
- txt = "Hello World!"
- size = draw.textsize(txt, ttf)
- draw.text((10, 10), txt, font=ttf)
- draw.rectangle((10, 10, 10 + size[0], 10 + size[1]))
+ target = "Tests/images/transparent_background_text_L.png"
+ assert_image_similar_tofile(im.convert("L"), target, 0.01)
- assert_image_similar_tofile(
- im, "Tests/images/rectangle_surrounding_text.png", 2.5
- )
- @pytest.mark.parametrize(
- "text, mode, font, size, length_basic, length_raqm",
- (
- # basic test
- ("text", "L", "FreeMono.ttf", 15, 36, 36),
- ("text", "1", "FreeMono.ttf", 15, 36, 36),
- # issue 4177
- ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875),
- ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875),
- # test 'l' not including extra margin
- # using exact value 2047 / 64 for raqm, checked with debugger
- ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
- ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
- ),
+def test_textbbox_equal(font):
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+
+ txt = "Hello World!"
+ bbox = draw.textbbox((10, 10), txt, font)
+ draw.text((10, 10), txt, font=font)
+ draw.rectangle(bbox)
+
+ assert_image_similar_tofile(im, "Tests/images/rectangle_surrounding_text.png", 2.5)
+
+
+@pytest.mark.parametrize(
+ "text, mode, fontname, size, length_basic, length_raqm",
+ (
+ # basic test
+ ("text", "L", "FreeMono.ttf", 15, 36, 36),
+ ("text", "1", "FreeMono.ttf", 15, 36, 36),
+ # issue 4177
+ ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875),
+ ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875),
+ # test 'l' not including extra margin
+ # using exact value 2047 / 64 for raqm, checked with debugger
+ ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
+ ("ill", "1", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375),
+ ),
+)
+def test_getlength(
+ text, mode, fontname, size, layout_engine, length_basic, length_raqm
+):
+ f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine)
+
+ im = Image.new(mode, (1, 1), 0)
+ d = ImageDraw.Draw(im)
+
+ if layout_engine == ImageFont.Layout.BASIC:
+ length = d.textlength(text, f)
+ assert length == length_basic
+ else:
+ # disable kerning, kerning metrics changed
+ length = d.textlength(text, f, features=["-kern"])
+ assert length == length_raqm
+
+
+def test_render_multiline(font):
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+ line_spacing = font.getbbox("A")[3] + 4
+ lines = TEST_TEXT.split("\n")
+ y = 0
+ for line in lines:
+ draw.text((0, y), line, font=font)
+ y += line_spacing
+
+ # some versions of freetype have different horizontal spacing.
+ # setting a tight epsilon, I'm showing the original test failure
+ # at epsilon = ~38.
+ assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
+
+
+def test_render_multiline_text(font):
+ # Test that text() correctly connects to multiline_text()
+ # and that align defaults to left
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+ draw.text((0, 0), TEST_TEXT, font=font)
+
+ assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01)
+
+ # Test that text() can pass on additional arguments
+ # to multiline_text()
+ draw.text(
+ (0, 0), TEST_TEXT, fill=None, font=font, anchor=None, spacing=4, align="left"
)
- def test_getlength(self, text, mode, font, size, length_basic, length_raqm):
- f = ImageFont.truetype(
- "Tests/fonts/" + font, size, layout_engine=self.LAYOUT_ENGINE
- )
+ draw.text((0, 0), TEST_TEXT, None, font, None, 4, "left")
- im = Image.new(mode, (1, 1), 0)
- d = ImageDraw.Draw(im)
- if self.LAYOUT_ENGINE == ImageFont.Layout.BASIC:
- length = d.textlength(text, f)
- assert length == length_basic
- else:
- # disable kerning, kerning metrics changed
- length = d.textlength(text, f, features=["-kern"])
- assert length == length_raqm
+@pytest.mark.parametrize(
+ "align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
+)
+def test_render_multiline_text_align(font, align, ext):
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+ draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align)
- def test_render_multiline(self):
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
- ttf = self.get_font()
- line_spacing = draw.textsize("A", font=ttf)[1] + 4
- lines = TEST_TEXT.split("\n")
- y = 0
- for line in lines:
- draw.text((0, y), line, font=ttf)
- y += line_spacing
+ assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01)
- # some versions of freetype have different horizontal spacing.
- # setting a tight epsilon, I'm showing the original test failure
- # at epsilon = ~38.
- assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
- def test_render_multiline_text(self):
- ttf = self.get_font()
+def test_unknown_align(font):
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
- # Test that text() correctly connects to multiline_text()
- # and that align defaults to left
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
- draw.text((0, 0), TEST_TEXT, font=ttf)
+ # Act/Assert
+ with pytest.raises(ValueError):
+ draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown")
- assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01)
- # Test that text() can pass on additional arguments
- # to multiline_text()
- draw.text(
- (0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, spacing=4, align="left"
- )
- draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left")
+def test_draw_align(font):
+ im = Image.new("RGB", (300, 100), "white")
+ draw = ImageDraw.Draw(im)
+ line = "some text"
+ draw.text((100, 40), line, (0, 0, 0), font=font, align="left")
- # Test align center and right
- for align, ext in {"center": "_center", "right": "_right"}.items():
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
- draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align)
- assert_image_similar_tofile(
- im, "Tests/images/multiline_text" + ext + ".png", 0.01
- )
-
- def test_unknown_align(self):
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
- ttf = self.get_font()
-
- # Act/Assert
- with pytest.raises(ValueError):
- draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align="unknown")
-
- def test_draw_align(self):
- im = Image.new("RGB", (300, 100), "white")
- draw = ImageDraw.Draw(im)
- ttf = self.get_font()
- line = "some text"
- draw.text((100, 40), line, (0, 0, 0), font=ttf, align="left")
-
- def test_multiline_size(self):
- ttf = self.get_font()
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
+def test_multiline_size(font):
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+ with pytest.warns(DeprecationWarning) as log:
# Test that textsize() correctly connects to multiline_textsize()
- assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize(
- TEST_TEXT, font=ttf
+ assert draw.textsize(TEST_TEXT, font=font) == draw.multiline_textsize(
+ TEST_TEXT, font=font
)
# Test that multiline_textsize corresponds to ImageFont.textsize()
# for single line text
- assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf)
+ assert font.getsize("A") == draw.multiline_textsize("A", font=font)
# Test that textsize() can pass on additional arguments
# to multiline_textsize()
- draw.textsize(TEST_TEXT, font=ttf, spacing=4)
- draw.textsize(TEST_TEXT, ttf, 4)
+ draw.textsize(TEST_TEXT, font=font, spacing=4)
+ draw.textsize(TEST_TEXT, font, 4)
+ assert len(log) == 6
- def test_multiline_width(self):
- ttf = self.get_font()
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
+def test_multiline_bbox(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=font) == draw.multiline_textbbox(
+ (0, 0), TEST_TEXT, font=font
+ )
+
+ # Test that multiline_textbbox corresponds to ImageFont.textbbox()
+ # for single line text
+ assert font.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=font)
+
+ # Test that textbbox() can pass on additional arguments
+ # to multiline_textbbox()
+ draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4)
+
+
+def test_multiline_width(font):
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+
+ assert (
+ draw.textbbox((0, 0), "longest line", font=font)[2]
+ == draw.multiline_textbbox((0, 0), "longest line\nline", font=font)[2]
+ )
+ with pytest.warns(DeprecationWarning) as log:
assert (
- draw.textsize("longest line", font=ttf)[0]
- == draw.multiline_textsize("longest line\nline", font=ttf)[0]
+ draw.textsize("longest line", font=font)[0]
+ == draw.multiline_textsize("longest line\nline", font=font)[0]
)
+ assert len(log) == 2
- def test_multiline_spacing(self):
- ttf = self.get_font()
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
- draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10)
+def test_multiline_spacing(font):
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+ draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10)
- assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5)
+ assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5)
- def test_rotated_transposed_font(self):
- img_grey = Image.new("L", (100, 100))
- draw = ImageDraw.Draw(img_grey)
- word = "testing"
- font = self.get_font()
- orientation = Image.Transpose.ROTATE_90
- transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
+@pytest.mark.parametrize(
+ "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
+)
+def test_rotated_transposed_font(font, orientation):
+ img_grey = Image.new("L", (100, 100))
+ draw = ImageDraw.Draw(img_grey)
+ word = "testing"
- # Original font
- draw.font = font
+ transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
+
+ # Original font
+ draw.font = font
+ with pytest.warns(DeprecationWarning) as log:
box_size_a = draw.textsize(word)
+ assert box_size_a == font.getsize(word)
+ assert len(log) == 2
+ bbox_a = draw.textbbox((10, 10), word)
- # Rotated font
- draw.font = transposed_font
+ # Rotated font
+ draw.font = transposed_font
+ with pytest.warns(DeprecationWarning) as log:
box_size_b = draw.textsize(word)
+ assert box_size_b == transposed_font.getsize(word)
+ assert len(log) == 2
+ bbox_b = draw.textbbox((20, 20), word)
- # Check (w,h) of box a is (h,w) of box b
- assert box_size_a[0] == box_size_b[1]
- assert box_size_a[1] == box_size_b[0]
+ # 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[1] == box_size_b[0]
- def test_unrotated_transposed_font(self):
- img_grey = Image.new("L", (100, 100))
- draw = ImageDraw.Draw(img_grey)
- word = "testing"
- font = self.get_font()
+ # 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]
- orientation = None
- transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
+ # text length is undefined for vertical text
+ pytest.raises(ValueError, draw.textlength, word)
- # Original font
- draw.font = font
+
+@pytest.mark.parametrize(
+ "orientation",
+ (
+ None,
+ Image.Transpose.ROTATE_180,
+ Image.Transpose.FLIP_LEFT_RIGHT,
+ Image.Transpose.FLIP_TOP_BOTTOM,
+ ),
+)
+def test_unrotated_transposed_font(font, orientation):
+ img_grey = Image.new("L", (100, 100))
+ draw = ImageDraw.Draw(img_grey)
+ word = "testing"
+
+ transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
+
+ # Original font
+ draw.font = font
+ with pytest.warns(DeprecationWarning) as log:
box_size_a = draw.textsize(word)
+ assert len(log) == 1
+ bbox_a = draw.textbbox((10, 10), word)
+ length_a = draw.textlength(word)
- # Rotated font
- draw.font = transposed_font
+ # Rotated font
+ draw.font = transposed_font
+ with pytest.warns(DeprecationWarning) as log:
box_size_b = draw.textsize(word)
+ assert len(log) == 1
+ bbox_b = draw.textbbox((20, 20), word)
+ length_b = draw.textlength(word)
- # Check boxes a and b are same size
- assert box_size_a == box_size_b
+ # Check boxes a and b are same size
+ assert box_size_a == box_size_b
- def test_rotated_transposed_font_get_mask(self):
- # Arrange
- text = "mask this"
- font = self.get_font()
- orientation = Image.Transpose.ROTATE_90
- transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
+ # 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]
- # Act
- mask = transposed_font.getmask(text)
+ assert length_a == length_b
- # Assert
- assert mask.size == (13, 108)
- def test_unrotated_transposed_font_get_mask(self):
- # Arrange
- text = "mask this"
- font = self.get_font()
- orientation = None
- transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
+@pytest.mark.parametrize(
+ "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
+)
+def test_rotated_transposed_font_get_mask(font, orientation):
+ # Arrange
+ text = "mask this"
+ transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
- # Act
- mask = transposed_font.getmask(text)
+ # Act
+ mask = transposed_font.getmask(text)
- # Assert
- assert mask.size == (108, 13)
+ # Assert
+ assert mask.size == (13, 108)
- def test_free_type_font_get_name(self):
- # Arrange
- font = self.get_font()
- # Act
- name = font.getname()
+@pytest.mark.parametrize(
+ "orientation",
+ (
+ None,
+ Image.Transpose.ROTATE_180,
+ Image.Transpose.FLIP_LEFT_RIGHT,
+ Image.Transpose.FLIP_TOP_BOTTOM,
+ ),
+)
+def test_unrotated_transposed_font_get_mask(font, orientation):
+ # Arrange
+ text = "mask this"
+ transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
- # Assert
- assert ("FreeMono", "Regular") == name
+ # Act
+ mask = transposed_font.getmask(text)
- def test_free_type_font_get_metrics(self):
- # Arrange
- font = self.get_font()
+ # Assert
+ assert mask.size == (108, 13)
- # Act
- ascent, descent = font.getmetrics()
- # Assert
- assert isinstance(ascent, int)
- assert isinstance(descent, int)
- assert (ascent, descent) == (16, 4) # too exact check?
+def test_free_type_font_get_name(font):
+ assert ("FreeMono", "Regular") == font.getname()
- def test_free_type_font_get_offset(self):
- # Arrange
- font = self.get_font()
- text = "offset this"
- # Act
+def test_free_type_font_get_metrics(font):
+ ascent, descent = font.getmetrics()
+
+ assert isinstance(ascent, int)
+ assert isinstance(descent, int)
+ assert (ascent, descent) == (16, 4)
+
+
+def test_free_type_font_get_offset(font):
+ # Arrange
+ text = "offset this"
+
+ # Act
+ with pytest.warns(DeprecationWarning) as log:
offset = font.getoffset(text)
- # Assert
- assert offset == (0, 3)
+ # Assert
+ assert len(log) == 1
+ assert offset == (0, 3)
- def test_free_type_font_get_mask(self):
- # Arrange
- font = self.get_font()
- text = "mask this"
- # Act
- mask = font.getmask(text)
+def test_free_type_font_get_mask(font):
+ # Arrange
+ text = "mask this"
- # Assert
- assert mask.size == (108, 13)
+ # Act
+ mask = font.getmask(text)
- def test_load_path_not_found(self):
- # Arrange
- filename = "somefilenamethatdoesntexist.ttf"
+ # Assert
+ assert mask.size == (108, 13)
- # Act/Assert
+
+def test_load_path_not_found():
+ # Arrange
+ filename = "somefilenamethatdoesntexist.ttf"
+
+ # Act/Assert
+ with pytest.raises(OSError):
+ ImageFont.load_path(filename)
+ with pytest.raises(OSError):
+ ImageFont.truetype(filename)
+
+
+def test_load_non_font_bytes():
+ with open("Tests/images/hopper.jpg", "rb") as f:
with pytest.raises(OSError):
- ImageFont.load_path(filename)
- with pytest.raises(OSError):
- ImageFont.truetype(filename)
+ ImageFont.truetype(f)
- def test_load_non_font_bytes(self):
- with open("Tests/images/hopper.jpg", "rb") as f:
- with pytest.raises(OSError):
- ImageFont.truetype(f)
- def test_default_font(self):
- # Arrange
- txt = 'This is a "better than nothing" default font.'
- im = Image.new(mode="RGB", size=(300, 100))
- draw = ImageDraw.Draw(im)
+def test_default_font():
+ # Arrange
+ txt = 'This is a "better than nothing" default font.'
+ im = Image.new(mode="RGB", size=(300, 100))
+ draw = ImageDraw.Draw(im)
- # Act
- default_font = ImageFont.load_default()
- draw.text((10, 10), txt, font=default_font)
+ # Act
+ default_font = ImageFont.load_default()
+ draw.text((10, 10), txt, font=default_font)
- # Assert
- assert_image_equal_tofile(im, "Tests/images/default_font.png")
+ # Assert
+ assert_image_equal_tofile(im, "Tests/images/default_font.png")
- def test_getsize_empty(self):
- # issue #2614
- font = self.get_font()
- # should not crash.
- assert (0, 0) == font.getsize("")
- def test_render_empty(self):
- # issue 2666
- font = self.get_font()
- im = Image.new(mode="RGB", size=(300, 100))
- target = im.copy()
- draw = ImageDraw.Draw(im)
- # should not crash here.
- draw.text((10, 10), "", font=font)
- assert_image_equal(im, target)
+def test_getbbox_empty(font):
+ # issue #2614, should not crash.
+ assert (0, 0, 0, 0) == font.getbbox("")
- def test_unicode_pilfont(self):
- # should not segfault, should return UnicodeDecodeError
- # issue #2826
- font = ImageFont.load_default()
- with pytest.raises(UnicodeEncodeError):
- font.getsize("’")
- def test_unicode_extended(self):
- # issue #3777
- text = "A\u278A\U0001F12B"
- target = "Tests/images/unicode_extended.png"
+def test_render_empty(font):
+ # issue 2666
+ im = Image.new(mode="RGB", size=(300, 100))
+ target = im.copy()
+ draw = ImageDraw.Draw(im)
+ # should not crash here.
+ draw.text((10, 10), "", font=font)
+ assert_image_equal(im, target)
- ttf = ImageFont.truetype(
- "Tests/fonts/NotoSansSymbols-Regular.ttf",
- FONT_SIZE,
- layout_engine=self.LAYOUT_ENGINE,
- )
- img = Image.new("RGB", (100, 60))
- d = ImageDraw.Draw(img)
- d.text((10, 10), text, font=ttf)
- # fails with 14.7
- assert_image_similar_tofile(img, target, 6.2)
+def test_unicode_pilfont():
+ # should not segfault, should return UnicodeDecodeError
+ # issue #2826
+ font = ImageFont.load_default()
+ with pytest.raises(UnicodeEncodeError):
+ font.getbbox("’")
- def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname):
+
+def test_unicode_extended(layout_engine):
+ # issue #3777
+ text = "A\u278A\U0001F12B"
+ target = "Tests/images/unicode_extended.png"
+
+ ttf = ImageFont.truetype(
+ "Tests/fonts/NotoSansSymbols-Regular.ttf",
+ FONT_SIZE,
+ layout_engine=layout_engine,
+ )
+ img = Image.new("RGB", (100, 60))
+ d = ImageDraw.Draw(img)
+ d.text((10, 10), text, font=ttf)
+
+ # fails with 14.7
+ assert_image_similar_tofile(img, target, 6.2)
+
+
+@pytest.mark.parametrize(
+ "platform, font_directory",
+ (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
+)
+@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
+def test_find_font(monkeypatch, platform, font_directory):
+ def _test_fake_loading_font(path_to_fake, fontname):
# Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with monkeypatch.context() as m:
@@ -478,520 +569,483 @@ class TestImageFont:
name = font.getname()
assert ("FreeMono", "Regular") == name
- @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
- def test_find_linux_font(self, monkeypatch):
- # A lot of mocking here - this is more for hitting code and
- # catching syntax like errors
- font_directory = "/usr/local/share/fonts"
- monkeypatch.setattr(sys, "platform", "linux")
+ # A lot of mocking here - this is more for hitting code and
+ # catching syntax like errors
+ monkeypatch.setattr(sys, "platform", platform)
+ if platform == "linux":
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
- def fake_walker(path):
- if path == font_directory:
- return [
- (
- path,
- [],
- ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"],
- )
- ]
- return [(path, [], ["some_random_font.ttf"])]
+ def fake_walker(path):
+ if path == font_directory:
+ return [
+ (
+ path,
+ [],
+ ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"],
+ )
+ ]
+ return [(path, [], ["some_random_font.ttf"])]
- monkeypatch.setattr(os, "walk", fake_walker)
- # Test that the font loads both with and without the
- # extension
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf"
+ monkeypatch.setattr(os, "walk", fake_walker)
+
+ # Test that the font loads both with and without the extension
+ _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf")
+ _test_fake_loading_font(font_directory + "/Arial.ttf", "Arial")
+
+ # Test that non-ttf fonts can be found without the extension
+ _test_fake_loading_font(font_directory + "/Single.otf", "Single")
+
+ # Test that ttf fonts are preferred if the extension is not specified
+ _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate")
+
+
+def test_imagefont_getters(font):
+ assert font.getmetrics() == (16, 4)
+ assert font.font.ascent == 16
+ assert font.font.descent == 4
+ assert font.font.height == 20
+ assert font.font.x_ppem == 20
+ assert font.font.y_ppem == 20
+ assert font.font.glyphs == 4177
+ assert font.getbbox("A") == (0, 4, 12, 16)
+ assert font.getbbox("AB") == (0, 4, 24, 16)
+ assert font.getbbox("M") == (0, 4, 12, 16)
+ assert font.getbbox("y") == (0, 7, 12, 20)
+ assert font.getbbox("a") == (0, 7, 12, 16)
+ assert font.getlength("A") == 12
+ assert font.getlength("AB") == 24
+ assert font.getlength("M") == 12
+ assert font.getlength("y") == 12
+ assert font.getlength("a") == 12
+ with pytest.warns(DeprecationWarning) as log:
+ assert font.getsize("A") == (12, 16)
+ assert font.getsize("AB") == (24, 16)
+ assert font.getsize("M") == (12, 16)
+ assert font.getsize("y") == (12, 20)
+ assert font.getsize("a") == (12, 16)
+ assert font.getsize_multiline("A") == (12, 16)
+ assert font.getsize_multiline("AB") == (24, 16)
+ assert font.getsize_multiline("a") == (12, 16)
+ assert font.getsize_multiline("ABC\n") == (36, 36)
+ assert font.getsize_multiline("ABC\nA") == (36, 36)
+ assert font.getsize_multiline("ABC\nAaaa") == (48, 36)
+ assert len(log) == 11
+
+
+@pytest.mark.parametrize("stroke_width", (0, 2))
+def test_getsize_stroke(font, stroke_width):
+ assert font.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 font.getsize("A", stroke_width=stroke_width) == (
+ 12 + stroke_width * 2,
+ 16 + stroke_width * 2,
)
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Arial.ttf", "Arial"
+ assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == (
+ 48 + stroke_width * 2,
+ 36 + stroke_width * 4,
)
+ assert len(log) == 2
- # Test that non-ttf fonts can be found without the
- # extension
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Single.otf", "Single"
- )
- # Test that ttf fonts are preferred if the extension is
- # not specified
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate"
- )
+def test_complex_font_settings():
+ t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC)
+ with pytest.raises(KeyError):
+ t.getmask("абвг", direction="rtl")
+ with pytest.raises(KeyError):
+ t.getmask("абвг", features=["-kern"])
+ with pytest.raises(KeyError):
+ t.getmask("абвг", language="sr")
- @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
- def test_find_macos_font(self, monkeypatch):
- # Like the linux test, more cover hitting code rather than testing
- # correctness.
- font_directory = "/System/Library/Fonts"
- monkeypatch.setattr(sys, "platform", "darwin")
- def fake_walker(path):
- if path == font_directory:
- return [
- (
- path,
- [],
- ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"],
- )
- ]
- return [(path, [], ["some_random_font.ttf"])]
-
- monkeypatch.setattr(os, "walk", fake_walker)
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf"
- )
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Arial.ttf", "Arial"
- )
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Single.otf", "Single"
- )
- self._test_fake_loading_font(
- monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate"
- )
-
- def test_imagefont_getters(self):
- # Arrange
- t = self.get_font()
-
- # Act / Assert
- assert t.getmetrics() == (16, 4)
- assert t.font.ascent == 16
- assert t.font.descent == 4
- assert t.font.height == 20
- assert t.font.x_ppem == 20
- assert t.font.y_ppem == 20
- assert t.font.glyphs == 4177
- assert t.getsize("A") == (12, 16)
- assert t.getsize("AB") == (24, 16)
- assert t.getsize("M") == (12, 16)
- assert t.getsize("y") == (12, 20)
- assert t.getsize("a") == (12, 16)
- assert t.getsize_multiline("A") == (12, 16)
- assert t.getsize_multiline("AB") == (24, 16)
- assert t.getsize_multiline("a") == (12, 16)
- assert t.getsize_multiline("ABC\n") == (36, 36)
- assert t.getsize_multiline("ABC\nA") == (36, 36)
- assert t.getsize_multiline("ABC\nAaaa") == (48, 36)
-
- def test_getsize_stroke(self):
- # Arrange
- t = self.get_font()
-
- # Act / Assert
- for stroke_width in [0, 2]:
- assert t.getsize("A", stroke_width=stroke_width) == (
- 12 + stroke_width * 2,
- 16 + stroke_width * 2,
- )
- assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == (
- 48 + stroke_width * 2,
- 36 + stroke_width * 4,
- )
-
- def test_complex_font_settings(self):
- # Arrange
- t = self.get_font()
- # Act / Assert
- if t.layout_engine == ImageFont.Layout.BASIC:
- with pytest.raises(KeyError):
- t.getmask("абвг", direction="rtl")
- with pytest.raises(KeyError):
- t.getmask("абвг", features=["-kern"])
- with pytest.raises(KeyError):
- t.getmask("абвг", language="sr")
-
- def test_variation_get(self):
- font = self.get_font()
-
- freetype = parse_version(features.version_module("freetype2"))
- if freetype < parse_version("2.9.1"):
- with pytest.raises(NotImplementedError):
- font.get_variation_names()
- with pytest.raises(NotImplementedError):
- font.get_variation_axes()
- return
-
- with pytest.raises(OSError):
+def test_variation_get(font):
+ freetype = parse_version(features.version_module("freetype2"))
+ if freetype < parse_version("2.9.1"):
+ with pytest.raises(NotImplementedError):
font.get_variation_names()
- with pytest.raises(OSError):
+ with pytest.raises(NotImplementedError):
font.get_variation_axes()
+ return
- font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf")
- assert font.get_variation_names(), [
- b"ExtraLight",
- b"Light",
- b"Regular",
- b"Semibold",
- b"Bold",
- b"Black",
- b"Black Medium Contrast",
- b"Black High Contrast",
- b"Default",
- ]
- assert font.get_variation_axes() == [
- {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389},
- {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0},
- ]
+ with pytest.raises(OSError):
+ font.get_variation_names()
+ with pytest.raises(OSError):
+ font.get_variation_axes()
- font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf")
- assert font.get_variation_names() == [
- b"20",
- b"40",
- b"60",
- b"80",
- b"100",
- b"120",
- b"140",
- b"160",
- b"180",
- b"200",
- b"220",
- b"240",
- b"260",
- b"280",
- b"300",
- b"Regular",
- ]
- assert font.get_variation_axes() == [
- {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0}
- ]
+ font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf")
+ assert font.get_variation_names(), [
+ b"ExtraLight",
+ b"Light",
+ b"Regular",
+ b"Semibold",
+ b"Bold",
+ b"Black",
+ b"Black Medium Contrast",
+ b"Black High Contrast",
+ b"Default",
+ ]
+ assert font.get_variation_axes() == [
+ {"name": b"Weight", "minimum": 200, "maximum": 900, "default": 389},
+ {"name": b"Contrast", "minimum": 0, "maximum": 100, "default": 0},
+ ]
- def _check_text(self, font, path, epsilon):
- im = Image.new("RGB", (100, 75), "white")
- d = ImageDraw.Draw(im)
- d.text((10, 10), "Text", font=font, fill="black")
+ font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf")
+ assert font.get_variation_names() == [
+ b"20",
+ b"40",
+ b"60",
+ b"80",
+ b"100",
+ b"120",
+ b"140",
+ b"160",
+ b"180",
+ b"200",
+ b"220",
+ b"240",
+ b"260",
+ b"280",
+ b"300",
+ b"Regular",
+ ]
+ assert font.get_variation_axes() == [
+ {"name": b"Size", "minimum": 0, "maximum": 300, "default": 0}
+ ]
- try:
+
+def _check_text(font, path, epsilon):
+ im = Image.new("RGB", (100, 75), "white")
+ d = ImageDraw.Draw(im)
+ d.text((10, 10), "Text", font=font, fill="black")
+
+ try:
+ assert_image_similar_tofile(im, path, epsilon)
+ except AssertionError:
+ if "_adobe" in path:
+ path = path.replace("_adobe", "_adobe_older_harfbuzz")
assert_image_similar_tofile(im, path, epsilon)
- except AssertionError:
- if "_adobe" in path:
- path = path.replace("_adobe", "_adobe_older_harfbuzz")
- assert_image_similar_tofile(im, path, epsilon)
- else:
- raise
-
- def test_variation_set_by_name(self):
- font = self.get_font()
-
- freetype = parse_version(features.version_module("freetype2"))
- if freetype < parse_version("2.9.1"):
- with pytest.raises(NotImplementedError):
- font.set_variation_by_name("Bold")
- return
-
- with pytest.raises(OSError):
- font.set_variation_by_name("Bold")
-
- font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
- self._check_text(font, "Tests/images/variation_adobe.png", 11)
- for name in ["Bold", b"Bold"]:
- font.set_variation_by_name(name)
- self._check_text(font, "Tests/images/variation_adobe_name.png", 11)
-
- font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
- self._check_text(font, "Tests/images/variation_tiny.png", 40)
- for name in ["200", b"200"]:
- font.set_variation_by_name(name)
- self._check_text(font, "Tests/images/variation_tiny_name.png", 40)
-
- def test_variation_set_by_axes(self):
- font = self.get_font()
-
- freetype = parse_version(features.version_module("freetype2"))
- if freetype < parse_version("2.9.1"):
- with pytest.raises(NotImplementedError):
- font.set_variation_by_axes([100])
- return
-
- with pytest.raises(OSError):
- font.set_variation_by_axes([500, 50])
-
- font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
- font.set_variation_by_axes([500, 50])
- self._check_text(font, "Tests/images/variation_adobe_axes.png", 11.05)
-
- font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
- font.set_variation_by_axes([100])
- self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
-
- def test_textbbox_non_freetypefont(self):
- im = Image.new("RGB", (200, 200))
- d = ImageDraw.Draw(im)
- default_font = ImageFont.load_default()
- with pytest.raises(ValueError):
- d.textbbox((0, 0), "test", font=default_font)
-
- @pytest.mark.parametrize(
- "anchor, left, top",
- (
- # test horizontal anchors
- ("ls", 0, -36),
- ("ms", -64, -36),
- ("rs", -128, -36),
- # test vertical anchors
- ("ma", -64, 16),
- ("mt", -64, 0),
- ("mm", -64, -17),
- ("mb", -64, -44),
- ("md", -64, -51),
- ),
- ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
- )
- def test_anchor(self, anchor, left, top):
- name, text = "quick", "Quick"
- path = f"Tests/images/test_anchor_{name}_{anchor}.png"
-
- if self.LAYOUT_ENGINE == ImageFont.Layout.RAQM:
- width, height = (129, 44)
else:
- width, height = (128, 44)
+ raise
- bbox_expected = (left, top, left + width, top + height)
- f = ImageFont.truetype(
- "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE
- )
+def test_variation_set_by_name(font):
+ freetype = parse_version(features.version_module("freetype2"))
+ if freetype < parse_version("2.9.1"):
+ with pytest.raises(NotImplementedError):
+ font.set_variation_by_name("Bold")
+ return
- im = Image.new("RGB", (200, 200), "white")
- d = ImageDraw.Draw(im)
- d.line(((0, 100), (200, 100)), "gray")
- d.line(((100, 0), (100, 200)), "gray")
- d.text((100, 100), text, fill="black", anchor=anchor, font=f)
+ with pytest.raises(OSError):
+ font.set_variation_by_name("Bold")
- assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected
+ font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
+ _check_text(font, "Tests/images/variation_adobe.png", 11)
+ for name in ["Bold", b"Bold"]:
+ font.set_variation_by_name(name)
+ _check_text(font, "Tests/images/variation_adobe_name.png", 11)
- assert_image_similar_tofile(im, path, 7)
+ font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
+ _check_text(font, "Tests/images/variation_tiny.png", 40)
+ for name in ["200", b"200"]:
+ font.set_variation_by_name(name)
+ _check_text(font, "Tests/images/variation_tiny_name.png", 40)
- @pytest.mark.parametrize(
- "anchor, align",
- (
- # test horizontal anchors
- ("lm", "left"),
- ("lm", "center"),
- ("lm", "right"),
- ("mm", "left"),
- ("mm", "center"),
- ("mm", "right"),
- ("rm", "left"),
- ("rm", "center"),
- ("rm", "right"),
- # test vertical anchors
- ("ma", "center"),
- # ("mm", "center"), # duplicate
- ("md", "center"),
- ),
+
+def test_variation_set_by_axes(font):
+ freetype = parse_version(features.version_module("freetype2"))
+ if freetype < parse_version("2.9.1"):
+ with pytest.raises(NotImplementedError):
+ font.set_variation_by_axes([100])
+ return
+
+ with pytest.raises(OSError):
+ font.set_variation_by_axes([500, 50])
+
+ font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36)
+ font.set_variation_by_axes([500, 50])
+ _check_text(font, "Tests/images/variation_adobe_axes.png", 11.05)
+
+ font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
+ font.set_variation_by_axes([100])
+ _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
+
+
+def test_textbbox_non_freetypefont():
+ im = Image.new("RGB", (200, 200))
+ d = ImageDraw.Draw(im)
+ default_font = ImageFont.load_default()
+ with pytest.warns(DeprecationWarning) as log:
+ 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(
+ "anchor, left, top",
+ (
+ # test horizontal anchors
+ ("ls", 0, -36),
+ ("ms", -64, -36),
+ ("rs", -128, -36),
+ # test vertical anchors
+ ("ma", -64, 16),
+ ("mt", -64, 0),
+ ("mm", -64, -17),
+ ("mb", -64, -44),
+ ("md", -64, -51),
+ ),
+ ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
+)
+def test_anchor(layout_engine, anchor, left, top):
+ name, text = "quick", "Quick"
+ path = f"Tests/images/test_anchor_{name}_{anchor}.png"
+
+ if layout_engine == ImageFont.Layout.RAQM:
+ width, height = (129, 44)
+ else:
+ width, height = (128, 44)
+
+ bbox_expected = (left, top, left + width, top + height)
+
+ f = ImageFont.truetype(
+ "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine
)
- def test_anchor_multiline(self, anchor, align):
- target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
- text = "a\nlong\ntext sample"
- f = ImageFont.truetype(
- "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=self.LAYOUT_ENGINE
+ im = Image.new("RGB", (200, 200), "white")
+ d = ImageDraw.Draw(im)
+ d.line(((0, 100), (200, 100)), "gray")
+ d.line(((100, 0), (100, 200)), "gray")
+ d.text((100, 100), text, fill="black", anchor=anchor, font=f)
+
+ assert d.textbbox((0, 0), text, f, anchor=anchor) == bbox_expected
+
+ assert_image_similar_tofile(im, path, 7)
+
+
+@pytest.mark.parametrize(
+ "anchor, align",
+ (
+ # test horizontal anchors
+ ("lm", "left"),
+ ("lm", "center"),
+ ("lm", "right"),
+ ("mm", "left"),
+ ("mm", "center"),
+ ("mm", "right"),
+ ("rm", "left"),
+ ("rm", "center"),
+ ("rm", "right"),
+ # test vertical anchors
+ ("ma", "center"),
+ # ("mm", "center"), # duplicate
+ ("md", "center"),
+ ),
+)
+def test_anchor_multiline(layout_engine, anchor, align):
+ target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png"
+ text = "a\nlong\ntext sample"
+
+ f = ImageFont.truetype(
+ "Tests/fonts/NotoSans-Regular.ttf", 48, layout_engine=layout_engine
+ )
+
+ # test render
+ im = Image.new("RGB", (600, 400), "white")
+ d = ImageDraw.Draw(im)
+ d.line(((0, 200), (600, 200)), "gray")
+ d.line(((300, 0), (300, 400)), "gray")
+ d.multiline_text((300, 200), text, fill="black", anchor=anchor, font=f, align=align)
+
+ assert_image_similar_tofile(im, target, 4)
+
+
+def test_anchor_invalid(font):
+ im = Image.new("RGB", (100, 100), "white")
+ d = ImageDraw.Draw(im)
+ d.font = font
+
+ for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
+ pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor))
+ pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor))
+ pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor))
+ pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor))
+ pytest.raises(
+ ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
+ )
+ pytest.raises(
+ ValueError,
+ lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
+ )
+ for anchor in ["lt", "lb"]:
+ pytest.raises(
+ ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
+ )
+ pytest.raises(
+ ValueError,
+ lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
)
- # test render
- im = Image.new("RGB", (600, 400), "white")
- d = ImageDraw.Draw(im)
- d.line(((0, 200), (600, 200)), "gray")
- d.line(((300, 0), (300, 400)), "gray")
- d.multiline_text(
- (300, 200), text, fill="black", anchor=anchor, font=f, align=align
- )
- assert_image_similar_tofile(im, target, 4)
+@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
+def test_bitmap_font(layout_engine, bpp):
+ text = "Bitmap Font"
+ layout_name = ["basic", "raqm"][layout_engine]
+ target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
+ font = ImageFont.truetype(
+ f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf",
+ 24,
+ layout_engine=layout_engine,
+ )
- def test_anchor_invalid(self):
- font = self.get_font()
- im = Image.new("RGB", (100, 100), "white")
- d = ImageDraw.Draw(im)
- d.font = font
+ im = Image.new("RGB", (160, 35), "white")
+ draw = ImageDraw.Draw(im)
+ draw.text((2, 2), text, "black", font)
- for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
- pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor))
- pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor))
- pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor))
- pytest.raises(
- ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)
- )
- pytest.raises(
- ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
- )
- pytest.raises(
- ValueError,
- lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
- )
- for anchor in ["lt", "lb"]:
- pytest.raises(
- ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
- )
- pytest.raises(
- ValueError,
- lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
- )
+ assert_image_equal_tofile(im, target)
- @skip_unless_feature("freetype2")
- @pytest.mark.parametrize("bpp", (1, 2, 4, 8))
- def test_bitmap_font(self, bpp):
- text = "Bitmap Font"
- layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE]
- target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png"
+
+def test_bitmap_font_stroke(layout_engine):
+ text = "Bitmap Font"
+ layout_name = ["basic", "raqm"][layout_engine]
+ target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
+ font = ImageFont.truetype(
+ "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf",
+ 24,
+ layout_engine=layout_engine,
+ )
+
+ im = Image.new("RGB", (160, 35), "white")
+ draw = ImageDraw.Draw(im)
+ draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red")
+
+ assert_image_similar_tofile(im, target, 0.03)
+
+
+def test_standard_embedded_color(layout_engine):
+ txt = "Hello World!"
+ ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
+ ttf.getbbox(txt)
+
+ im = Image.new("RGB", (300, 64), "white")
+ d = ImageDraw.Draw(im)
+ d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True)
+
+ assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2)
+
+
+def test_cbdt(layout_engine):
+ try:
font = ImageFont.truetype(
- f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf",
- 24,
- layout_engine=self.LAYOUT_ENGINE,
+ "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
)
- im = Image.new("RGB", (160, 35), "white")
- draw = ImageDraw.Draw(im)
- draw.text((2, 2), text, "black", font)
-
- assert_image_equal_tofile(im, target)
-
- def test_bitmap_font_stroke(self):
- text = "Bitmap Font"
- layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE]
- target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
- font = ImageFont.truetype(
- "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf",
- 24,
- layout_engine=self.LAYOUT_ENGINE,
- )
-
- im = Image.new("RGB", (160, 35), "white")
- draw = ImageDraw.Draw(im)
- draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red")
-
- assert_image_similar_tofile(im, target, 0.03)
-
- def test_standard_embedded_color(self):
- txt = "Hello World!"
- ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE)
- ttf.getsize(txt)
-
- im = Image.new("RGB", (300, 64), "white")
- d = ImageDraw.Draw(im)
- d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True)
-
- assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2)
-
- def test_cbdt(self):
- try:
- font = ImageFont.truetype(
- "Tests/fonts/NotoColorEmoji.ttf",
- size=109,
- layout_engine=self.LAYOUT_ENGINE,
- )
-
- im = Image.new("RGB", (150, 150), "white")
- d = ImageDraw.Draw(im)
-
- d.text((10, 10), "\U0001f469", font=font, embedded_color=True)
-
- assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2)
- except OSError as e: # pragma: no cover
- assert str(e) in ("unimplemented feature", "unknown file format")
- pytest.skip("freetype compiled without libpng or CBDT support")
-
- def test_cbdt_mask(self):
- try:
- font = ImageFont.truetype(
- "Tests/fonts/NotoColorEmoji.ttf",
- size=109,
- layout_engine=self.LAYOUT_ENGINE,
- )
-
- im = Image.new("RGB", (150, 150), "white")
- d = ImageDraw.Draw(im)
-
- d.text((10, 10), "\U0001f469", "black", font=font)
-
- assert_image_similar_tofile(
- im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
- )
- except OSError as e: # pragma: no cover
- assert str(e) in ("unimplemented feature", "unknown file format")
- pytest.skip("freetype compiled without libpng or CBDT support")
-
- def test_sbix(self):
- try:
- font = ImageFont.truetype(
- "Tests/fonts/chromacheck-sbix.woff",
- size=300,
- layout_engine=self.LAYOUT_ENGINE,
- )
-
- im = Image.new("RGB", (400, 400), "white")
- d = ImageDraw.Draw(im)
-
- d.text((50, 50), "\uE901", font=font, embedded_color=True)
-
- assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
- except OSError as e: # pragma: no cover
- assert str(e) in ("unimplemented feature", "unknown file format")
- pytest.skip("freetype compiled without libpng or SBIX support")
-
- def test_sbix_mask(self):
- try:
- font = ImageFont.truetype(
- "Tests/fonts/chromacheck-sbix.woff",
- size=300,
- layout_engine=self.LAYOUT_ENGINE,
- )
-
- im = Image.new("RGB", (400, 400), "white")
- d = ImageDraw.Draw(im)
-
- d.text((50, 50), "\uE901", (100, 0, 0), font=font)
-
- assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
- except OSError as e: # pragma: no cover
- assert str(e) in ("unimplemented feature", "unknown file format")
- pytest.skip("freetype compiled without libpng or SBIX support")
-
- @skip_unless_feature_version("freetype2", "2.10.0")
- def test_colr(self):
- font = ImageFont.truetype(
- "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
- size=64,
- layout_engine=self.LAYOUT_ENGINE,
- )
-
- im = Image.new("RGB", (300, 75), "white")
+ im = Image.new("RGB", (150, 150), "white")
d = ImageDraw.Draw(im)
- d.text((15, 5), "Bungee", font=font, embedded_color=True)
+ d.text((10, 10), "\U0001f469", font=font, embedded_color=True)
- assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21)
+ assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2)
+ except OSError as e: # pragma: no cover
+ assert str(e) in ("unimplemented feature", "unknown file format")
+ pytest.skip("freetype compiled without libpng or CBDT support")
- @skip_unless_feature_version("freetype2", "2.10.0")
- def test_colr_mask(self):
+
+def test_cbdt_mask(layout_engine):
+ try:
font = ImageFont.truetype(
- "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
- size=64,
- layout_engine=self.LAYOUT_ENGINE,
+ "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
)
- im = Image.new("RGB", (300, 75), "white")
+ im = Image.new("RGB", (150, 150), "white")
d = ImageDraw.Draw(im)
- d.text((15, 5), "Bungee", "black", font=font)
+ d.text((10, 10), "\U0001f469", "black", font=font)
- assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
-
- def test_fill_deprecation(self):
- font = self.get_font()
- with pytest.warns(DeprecationWarning):
- font.getmask2("Hello world", fill=Image.core.fill)
- with pytest.warns(DeprecationWarning):
- with pytest.raises(TypeError):
- font.getmask2("Hello world", fill=None)
+ assert_image_similar_tofile(
+ im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
+ )
+ except OSError as e: # pragma: no cover
+ assert str(e) in ("unimplemented feature", "unknown file format")
+ pytest.skip("freetype compiled without libpng or CBDT support")
-@skip_unless_feature("raqm")
-class TestImageFont_RaqmLayout(TestImageFont):
- LAYOUT_ENGINE = ImageFont.Layout.RAQM
+def test_sbix(layout_engine):
+ try:
+ font = ImageFont.truetype(
+ "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
+ )
+
+ im = Image.new("RGB", (400, 400), "white")
+ d = ImageDraw.Draw(im)
+
+ d.text((50, 50), "\uE901", font=font, embedded_color=True)
+
+ assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
+ except OSError as e: # pragma: no cover
+ assert str(e) in ("unimplemented feature", "unknown file format")
+ pytest.skip("freetype compiled without libpng or SBIX support")
+
+
+def test_sbix_mask(layout_engine):
+ try:
+ font = ImageFont.truetype(
+ "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine
+ )
+
+ im = Image.new("RGB", (400, 400), "white")
+ d = ImageDraw.Draw(im)
+
+ d.text((50, 50), "\uE901", (100, 0, 0), font=font)
+
+ assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
+ except OSError as e: # pragma: no cover
+ assert str(e) in ("unimplemented feature", "unknown file format")
+ pytest.skip("freetype compiled without libpng or SBIX support")
+
+
+@skip_unless_feature_version("freetype2", "2.10.0")
+def test_colr(layout_engine):
+ font = ImageFont.truetype(
+ "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
+ size=64,
+ layout_engine=layout_engine,
+ )
+
+ im = Image.new("RGB", (300, 75), "white")
+ d = ImageDraw.Draw(im)
+
+ d.text((15, 5), "Bungee", font=font, embedded_color=True)
+
+ assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21)
+
+
+@skip_unless_feature_version("freetype2", "2.10.0")
+def test_colr_mask(layout_engine):
+ font = ImageFont.truetype(
+ "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf",
+ size=64,
+ layout_engine=layout_engine,
+ )
+
+ im = Image.new("RGB", (300, 75), "white")
+ d = ImageDraw.Draw(im)
+
+ d.text((15, 5), "Bungee", "black", font=font)
+
+ assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
+
+
+def test_fill_deprecation(font):
+ with pytest.warns(DeprecationWarning):
+ font.getmask2("Hello world", fill=Image.core.fill)
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(TypeError):
+ font.getmask2("Hello world", fill=None)
def test_render_mono_size():
diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py
index ffb70cf17..cf039e86e 100644
--- a/Tests/test_imagefontctl.py
+++ b/Tests/test_imagefontctl.py
@@ -140,8 +140,8 @@ def test_ligature_features():
target = "Tests/images/test_ligature_features.png"
assert_image_similar_tofile(im, target, 0.5)
- liga_size = ttf.getsize("fi", features=["-liga"])
- assert liga_size == (13, 19)
+ liga_bbox = ttf.getbbox("fi", features=["-liga"])
+ assert liga_bbox == (0, 4, 13, 19)
def test_kerning_features():
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index 6de953068..29c71f917 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -65,14 +65,16 @@ def create_lut():
# create_lut()
-def test_lut():
- for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"):
- lb = ImageMorph.LutBuilder(op_name=op)
- assert lb.get_lut() is None
+@pytest.mark.parametrize(
+ "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
+)
+def test_lut(op):
+ lb = ImageMorph.LutBuilder(op_name=op)
+ assert lb.get_lut() is None
- lut = lb.build_lut()
- with open(f"Tests/images/{op}.lut", "rb") as f:
- assert lut == bytearray(f.read())
+ lut = lb.build_lut()
+ with open(f"Tests/images/{op}.lut", "rb") as f:
+ assert lut == bytearray(f.read())
def test_no_operator_loaded():
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 87fffa7b7..c9b2fd865 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -110,6 +110,16 @@ def test_contain(new_size):
assert new_im.size == (256, 256)
+def test_contain_round():
+ im = Image.new("1", (43, 63), 1)
+ new_im = ImageOps.contain(im, (5, 7))
+ assert new_im.width == 5
+
+ im = Image.new("1", (63, 43), 1)
+ new_im = ImageOps.contain(im, (7, 5))
+ assert new_im.height == 5
+
+
def test_pad():
# Same ratio
im = hopper()
@@ -130,6 +140,30 @@ def test_pad():
)
+def test_pad_round():
+ im = Image.new("1", (1, 1), 1)
+ new_im = ImageOps.pad(im, (4, 1))
+ assert new_im.load()[2, 0] == 1
+
+ new_im = ImageOps.pad(im, (1, 4))
+ assert new_im.load()[0, 2] == 1
+
+
+@pytest.mark.parametrize("mode", ("P", "PA"))
+def test_palette(mode):
+ im = hopper(mode)
+
+ # Expand
+ expanded_im = ImageOps.expand(im)
+ assert_image_equal(im.convert("RGB"), expanded_im.convert("RGB"))
+
+ # Pad
+ padded_im = ImageOps.pad(im, (256, 128), centering=(0, 0))
+ assert_image_equal(
+ im.convert("RGB"), padded_im.convert("RGB").crop((0, 0, 128, 128))
+ )
+
+
def test_pil163():
# Division by zero in equalize if < 255 pixels in image (@PIL163)
@@ -345,11 +379,15 @@ def test_exif_transpose():
check(orientation_im)
# Orientation from "XML:com.adobe.xmp" info key
- with Image.open("Tests/images/xmp_tags_orientation.png") as im:
- assert im.getexif()[0x0112] == 3
+ for suffix in ("", "_exiftool"):
+ with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im:
+ assert im.getexif()[0x0112] == 3
- transposed_im = ImageOps.exif_transpose(im)
- assert 0x0112 not in transposed_im.getexif()
+ transposed_im = ImageOps.exif_transpose(im)
+ 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
# This test image has been manually hexedited from exif_imagemagick.png
diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py
index 55d7c9479..3e147a9ef 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -45,10 +45,10 @@ def test_viewer_show(order):
not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs",
)
-def test_show():
- for mode in ("1", "I;16", "LA", "RGB", "RGBA"):
- im = hopper(mode)
- assert ImageShow.show(im)
+@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
+def test_show(mode):
+ im = hopper(mode)
+ assert ImageShow.show(im)
def test_show_without_viewers():
@@ -70,12 +70,12 @@ def test_viewer():
viewer.get_command(None)
-def test_viewers():
- for viewer in ImageShow._viewers:
- try:
- viewer.get_command("test.jpg")
- except NotImplementedError:
- pass
+@pytest.mark.parametrize("viewer", ImageShow._viewers)
+def test_viewers(viewer):
+ try:
+ viewer.get_command("test.jpg")
+ except NotImplementedError:
+ pass
def test_ipythonviewer():
@@ -95,14 +95,14 @@ def test_ipythonviewer():
not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs",
)
-def test_file_deprecated(tmp_path):
+@pytest.mark.parametrize("viewer", ImageShow._viewers)
+def test_file_deprecated(tmp_path, viewer):
f = str(tmp_path / "temp.jpg")
- for viewer in ImageShow._viewers:
- hopper().save(f)
- with pytest.warns(DeprecationWarning):
- try:
- viewer.show_file(file=f)
- except NotImplementedError:
- pass
- with pytest.raises(TypeError):
- viewer.show_file()
+ hopper().save(f)
+ with pytest.warns(DeprecationWarning):
+ try:
+ viewer.show_file(file=f)
+ except NotImplementedError:
+ pass
+ with pytest.raises(TypeError):
+ viewer.show_file()
diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py
index a929910b3..995d0ee1f 100644
--- a/Tests/test_imagetk.py
+++ b/Tests/test_imagetk.py
@@ -54,32 +54,39 @@ def test_kw():
assert im is None
-def test_photoimage():
- for mode in TK_MODES:
- # test as image:
- im = hopper(mode)
+@pytest.mark.parametrize("mode", TK_MODES)
+def test_photoimage(mode):
+ # test as image:
+ im = hopper(mode)
- # this should not crash
+ # this should not crash
+ im_tk = ImageTk.PhotoImage(im)
+
+ assert im_tk.width() == im.width
+ assert im_tk.height() == im.height
+
+ reloaded = ImageTk.getimage(im_tk)
+ assert_image_equal(reloaded, im.convert("RGBA"))
+
+
+def test_photoimage_apply_transparency():
+ with Image.open("Tests/images/pil123p.png") as im:
im_tk = ImageTk.PhotoImage(im)
-
- assert im_tk.width() == im.width
- assert im_tk.height() == im.height
-
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA"))
-def test_photoimage_blank():
+@pytest.mark.parametrize("mode", TK_MODES)
+def test_photoimage_blank(mode):
# test a image using mode/size:
- for mode in TK_MODES:
- im_tk = ImageTk.PhotoImage(mode, (100, 100))
+ im_tk = ImageTk.PhotoImage(mode, (100, 100))
- assert im_tk.width() == 100
- assert im_tk.height() == 100
+ assert im_tk.width() == 100
+ assert im_tk.height() == 100
- im = Image.new(mode, (100, 100))
- reloaded = ImageTk.getimage(im_tk)
- assert_image_equal(reloaded.convert(mode), im)
+ im = Image.new(mode, (100, 100))
+ reloaded = ImageTk.getimage(im_tk)
+ assert_image_equal(reloaded.convert(mode), im)
def test_box_deprecation():
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index 6e8a2ac58..efcdab9ec 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image
from .helper import hopper
@@ -20,65 +22,56 @@ def verify(im1):
), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}"
-def test_basic(tmp_path):
+@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I"))
+def test_basic(tmp_path, mode):
# PIL 1.1 has limited support for 16-bit image data. Check that
# create/copy/transform and save works as expected.
- def basic(mode):
+ im_in = original.convert(mode)
+ verify(im_in)
- im_in = original.convert(mode)
- verify(im_in)
+ w, h = im_in.size
- w, h = im_in.size
+ im_out = im_in.copy()
+ verify(im_out) # copy
- im_out = im_in.copy()
- verify(im_out) # copy
+ im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
+ verify(im_out) # transform
- im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
- verify(im_out) # transform
+ filename = str(tmp_path / "temp.im")
+ im_in.save(filename)
- filename = str(tmp_path / "temp.im")
- im_in.save(filename)
-
- with Image.open(filename) as im_out:
-
- verify(im_in)
- verify(im_out)
-
- im_out = im_in.crop((0, 0, w, h))
- verify(im_out)
-
- im_out = Image.new(mode, (w, h), None)
- im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
- im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
+ with Image.open(filename) as im_out:
verify(im_in)
verify(im_out)
- im_in = Image.new(mode, (1, 1), 1)
- assert im_in.getpixel((0, 0)) == 1
+ im_out = im_in.crop((0, 0, w, h))
+ verify(im_out)
- im_in.putpixel((0, 0), 2)
- assert im_in.getpixel((0, 0)) == 2
+ im_out = Image.new(mode, (w, h), None)
+ im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
+ im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
- if mode == "L":
- maximum = 255
- else:
- maximum = 32767
+ verify(im_in)
+ verify(im_out)
- im_in = Image.new(mode, (1, 1), 256)
- assert im_in.getpixel((0, 0)) == min(256, maximum)
+ im_in = Image.new(mode, (1, 1), 1)
+ assert im_in.getpixel((0, 0)) == 1
- im_in.putpixel((0, 0), 512)
- assert im_in.getpixel((0, 0)) == min(512, maximum)
+ im_in.putpixel((0, 0), 2)
+ assert im_in.getpixel((0, 0)) == 2
- basic("L")
+ if mode == "L":
+ maximum = 255
+ else:
+ maximum = 32767
- basic("I;16")
- basic("I;16B")
- basic("I;16L")
+ im_in = Image.new(mode, (1, 1), 256)
+ assert im_in.getpixel((0, 0)) == min(256, maximum)
- basic("I")
+ im_in.putpixel((0, 0), 512)
+ assert im_in.getpixel((0, 0)) == min(512, maximum)
def test_tobytes():
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 9735837bc..185e477ec 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -137,19 +137,9 @@ def test_save_tiff_uint16():
assert img_px[0, 0] == pixel_value
-def test_to_array():
- def _to_array(mode, dtype):
- img = hopper(mode)
-
- # Resize to non-square
- img = img.crop((3, 0, 124, 127))
- assert img.size == (121, 127)
-
- np_img = numpy.array(img)
- _test_img_equals_nparray(img, np_img)
- assert np_img.dtype == dtype
-
- modes = [
+@pytest.mark.parametrize(
+ "mode, dtype",
+ (
("L", numpy.uint8),
("I", numpy.int32),
("F", numpy.float32),
@@ -163,10 +153,18 @@ def test_to_array():
("I;16B", ">u2"),
("I;16L", " /dev/null 2>&1 || python3 -m pip install sphinx
+ $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile
html:
$(MAKE) install-sphinx
@@ -178,4 +179,4 @@ livehtml: html
livereload $(BUILDDIR)/html -p 33233
serve:
- cd $(BUILDDIR)/html; python3 -m http.server
+ cd $(BUILDDIR)/html; $(PYTHON) -m http.server
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 8c5b8a748..dec652df8 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -178,6 +178,61 @@ Image.coerce_e
This undocumented method has been deprecated and will be removed in Pillow 10
(2023-07-01).
+.. _Font size and offset methods:
+
+Font size and offset methods
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 9.2.0
+
+Several functions for computing the size and offset of rendered text
+have been deprecated and will be removed in Pillow 10 (2023-07-01):
+
+=========================================================================== =============================================================================================================
+Deprecated Use instead
+=========================================================================== =============================================================================================================
+:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength`
+:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox`
+:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength`
+:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength`
+:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox`
+:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
+=========================================================================== =============================================================================================================
+
+Previous code:
+
+.. code-block:: python
+
+ from PIL import Image, ImageDraw, ImageFont
+
+ font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+ width, height = font.getsize("Hello world")
+ left, top = font.getoffset("Hello world")
+
+ im = Image.new("RGB", (100, 100))
+ draw = ImageDraw.Draw(im)
+ width, height = draw.textsize("Hello world")
+
+ width, height = font.getsize_multiline("Hello\nworld")
+ width, height = draw.multiline_textsize("Hello\nworld")
+
+Use instead:
+
+.. code-block:: python
+
+ from PIL import Image, ImageDraw, ImageFont
+
+ font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+ left, top, right, bottom = font.getbbox("Hello world")
+ width, height = right - left, bottom - top
+
+ im = Image.new("RGB", (100, 100))
+ draw = ImageDraw.Draw(im)
+ width = draw.textlength("Hello world")
+
+ left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld")
+ width, height = right - left, bottom - top
+
Removed features
----------------
@@ -234,7 +289,7 @@ Support for FreeType 2.7 has been removed.
We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe
vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
-.. _FreeType: https://www.freetype.org
+.. _FreeType: https://freetype.org/
im.offset
~~~~~~~~~
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 1826d965f..dc629666c 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -31,6 +31,9 @@ BLP is the Blizzard Mipmap Format, a texture format used in World of
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
images, and all types of ``BLP2`` images.
+Saving
+~~~~~~
+
Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method
can take the following keyword arguments:
@@ -46,6 +49,9 @@ or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length enc
is not supported. Support for reading 8-bit run-length encoding was added in Pillow
9.1.0.
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@@ -78,6 +84,9 @@ EPS images. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and
than leaving them in the original color space. The EPS driver can write images
in ``L``, ``RGB`` and ``CMYK`` modes.
+Loading
+~~~~~~~
+
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
method with the following parameters to affect how Ghostscript renders the EPS
@@ -134,6 +143,11 @@ To restore the default behavior, where ``P`` mode images are only converted to
from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
+.. _gif-opening:
+
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@@ -171,6 +185,8 @@ to seek to the next frame (``im.seek(im.tell() + 1)``).
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame.
+.. _gif-saving:
+
Saving
~~~~~~
@@ -278,6 +294,11 @@ sets the following :py:attr:`~PIL.Image.Image.info` property:
ask for ``(512, 512, 2)``, the final value of
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``).
+.. _icns-saving:
+
+Saving
+~~~~~~
+
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**append_images**
@@ -292,6 +313,11 @@ ICO
ICO is used to store icons on Windows. The largest available icon is read.
+.. _ico-saving:
+
+Saving
+~~~~~~
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**sizes**
@@ -337,6 +363,11 @@ their original size while loading them.
By default Pillow doesn't allow loading of truncated JPEG files, set
:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this.
+.. _jpeg-opening:
+
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method may set the following
:py:attr:`~PIL.Image.Image.info` properties if available:
@@ -383,6 +414,10 @@ The :py:meth:`~PIL.Image.open` method may set the following
.. versionadded:: 7.1.0
+.. _jpeg-saving:
+
+Saving
+~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
@@ -464,6 +499,11 @@ itself. It is also possible to set ``reduce`` to the number of resolutions to
discard (each one reduces the size of the resulting image by a factor of 2),
and ``layers`` to specify the number of quality layers to load.
+.. _jpeg-2000-saving:
+
+Saving
+~~~~~~
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**offset**
@@ -575,6 +615,11 @@ called.
By default Pillow doesn't allow loading of truncated PNG files, set
:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this.
+.. _png-opening:
+
+Opening
+~~~~~~~
+
The :py:func:`~PIL.Image.open` function sets the following
:py:attr:`~PIL.Image.Image.info` properties, when appropriate:
@@ -613,6 +658,11 @@ decompression bombs. Additionally, the total size of all of the text
chunks is limited to :data:`.PngImagePlugin.MAX_TEXT_MEMORY`, defaulting to
64MB.
+.. _png-saving:
+
+Saving
+~~~~~~
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**optimize**
@@ -803,6 +853,11 @@ Pillow also reads SPIDER stack files containing sequences of SPIDER images. The
:py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and
random access is allowed.
+.. _spider-opening:
+
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method sets the following attributes:
**format**
@@ -819,19 +874,18 @@ is provided for converting floating point data to byte data (mode ``L``)::
im = Image.open("image001.spi").convert2byte()
-Writing files in SPIDER format
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. _spider-saving:
+
+Saving
+~~~~~~
The extension of SPIDER files may be any 3 alphanumeric characters. Therefore
the output format must be specified explicitly::
im.save('newimage.spi', format='SPIDER')
-For more information about the SPIDER image processing package, see the
-`SPIDER homepage`_ at `Wadsworth Center`_.
-
-.. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html
-.. _Wadsworth Center: https://www.wadsworth.org/
+For more information about the SPIDER image processing package, see
+https://github.com/spider-em/SPIDER
TGA
^^^
@@ -840,6 +894,29 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``,
``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and
run-length encoded TGAs.
+.. _tga-saving:
+
+Saving
+~~~~~~
+
+The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
+
+**compression**
+ If set to "tga_rle", the file will be run-length encoded.
+
+ .. versionadded:: 5.3.0
+
+**id_section**
+ The identification field.
+
+ .. versionadded:: 5.3.0
+
+**orientation**
+ If present and a positive number, the first pixel is for the top left corner,
+ rather than the bottom left corner.
+
+ .. versionadded:: 5.3.0
+
TIFF
^^^^
@@ -856,6 +933,11 @@ uncompressed files.
support for reading Packbits, LZW and JPEG compressed TIFFs
without using libtiff.
+.. _tiff-opening:
+
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@@ -907,8 +989,10 @@ and can be accessed in any order.
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
last frame.
-Saving Tiff Images
-~~~~~~~~~~~~~~~~~~
+.. _tiff-saving:
+
+Saving
+~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
@@ -971,7 +1055,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``,
``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``,
``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``,
- ``"webp"`, ``"zstd"``
+ ``"webp"``, ``"zstd"``
**quality**
The image quality for JPEG compression, on a scale from 0 (worst) to 100
@@ -1020,6 +1104,11 @@ WebP
Pillow reads and writes WebP files. The specifics of Pillow's capabilities with
this format are currently undocumented.
+.. _webp-saving:
+
+Saving
+~~~~~~
+
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**lossless**
@@ -1043,7 +1132,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
the system WebP library was built with webpmux support.
Saving sequences
-~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~
.. note::
@@ -1158,6 +1247,11 @@ GBR
The GBR decoder reads GIMP brush files, version 1 and 2.
+.. _gbr-opening:
+
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@@ -1173,6 +1267,11 @@ GD
Pillow reads uncompressed GD2 files. Note that you must use
:py:func:`PIL.GdImageFile.open` to read such a file.
+.. _gd-opening:
+
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@@ -1212,6 +1311,22 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL
methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported.
+.. _mpo-saving:
+
+Saving
+~~~~~~
+
+When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
+only the first frame of a multiframe image will be saved. If the ``save_all``
+argument is present and true, then all frames will be saved, and the following
+option will also be available.
+
+**append_images**
+ A list of images to append as additional pictures. Each of the
+ images in the list can be single or multiframe images.
+
+ .. versionadded:: 9.3.0
+
PCD
^^^
@@ -1300,6 +1415,11 @@ XPM
Pillow reads X pixmap files (mode ``P``) with 256 colors or less.
+.. _xpm-opening:
+
+Opening
+~~~~~~~
+
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@@ -1324,6 +1444,11 @@ Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4
files, using either JPEG or HEX encoding depending on the image mode (and
whether JPEG support is available or not).
+.. _pdf-saving:
+
+Saving
+~~~~~~
+
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**save_all**
diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst
index 80138742d..323127e5b 100644
--- a/docs/handbook/writing-your-own-image-plugin.rst
+++ b/docs/handbook/writing-your-own-image-plugin.rst
@@ -141,6 +141,10 @@ The fields are used as follows:
uncompressed data, in a variety of pixel formats. For more information on
this decoder, see the description below.
+ A list of C decoders can be seen under codecs section of the function array
+ in :file:`_imaging.c`. Python decoders are registered within the relevant
+ plugins.
+
**region**
A 4-tuple specifying where to store data in the image.
diff --git a/docs/index.rst b/docs/index.rst
index 5e886c2e8..45af4c571 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -69,6 +69,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 9.0 | Yes | Yes | Yes | Yes | | | | |
-+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | |
-+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
-+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
-+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes |
-+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes |
-+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes |
-+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+.. csv-table:: Newer versions
+ :file: newer-versions.csv
+ :header-rows: 1
-+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
-| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 |
-+==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+
-| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | |
-+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | |
-+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | |
-+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
-| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes |
-+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+.. csv-table:: Older versions
+ :file: older-versions.csv
+ :header-rows: 1
Basic Installation
------------------
@@ -188,7 +166,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
- * Pillow has been tested with libimagequant **2.6-4.0**
+ * Pillow has been tested with libimagequant **2.6-4.0.4**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.
@@ -206,7 +184,7 @@ Many of Pillow's features require external libraries:
loads libfribidi at runtime if it is installed.
On Windows this requires compiling FriBiDi and installing ``fribidi.dll``
into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs)
- `_
+ `_
(``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected).
See `Build Options`_ to see how to build this version.
* Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime.
@@ -389,7 +367,7 @@ In Alpine, the command is::
.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
-Prerequisites for **Ubuntu 16.04 LTS - 20.04 LTS** are installed with::
+Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
@@ -504,11 +482,11 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+
-| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |arm |
+| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+--------------+
-| | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |x86-64 |
+| | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |x86-64 |
| +---------------------------+------------------+ |
| | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+
diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv
new file mode 100644
index 000000000..ed2369259
--- /dev/null
+++ b/docs/newer-versions.csv
@@ -0,0 +1,6 @@
+Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5
+Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,
+Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,
+Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,
+Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,
+Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes
diff --git a/docs/older-versions.csv b/docs/older-versions.csv
new file mode 100644
index 000000000..6058f0524
--- /dev/null
+++ b/docs/older-versions.csv
@@ -0,0 +1,8 @@
+Python,3.8,3.7,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4
+Pillow 6.2.1 - 6.2.2,Yes,Yes,Yes,Yes,,,,Yes,,,
+Pillow 6.0 - 6.2.0,,Yes,Yes,Yes,,,,Yes,,,
+Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,,
+Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,,
+Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,,
+Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,,
+Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes
\ No newline at end of file
diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst
index 4567d4d3e..794fa238f 100644
--- a/docs/reference/ExifTags.rst
+++ b/docs/reference/ExifTags.rst
@@ -10,7 +10,7 @@ provide constants and clear-text names for various well-known EXIF tags.
.. py:data:: TAGS
:type: dict
- The TAG dictionary maps 16-bit integer EXIF tag enumerations to
+ The TAGS dictionary maps 16-bit integer EXIF tag enumerations to
descriptive string names. For instance:
>>> from PIL.ExifTags import TAGS
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index ed37521fd..7f6f666c3 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -53,9 +53,9 @@ Functions
To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files
which decompress into a huge amount of data and are designed to crash or cause disruption by using up
a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an
- image is over a certain limit, :py:data:`PIL.Image.MAX_IMAGE_PIXELS`.
+ image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`.
- This threshold can be changed by setting :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. It can be disabled
+ This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled
by setting ``Image.MAX_IMAGE_PIXELS = None``.
If desired, the warning can be turned into an error with
@@ -63,7 +63,7 @@ Functions
``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also
`the logging documentation`_ to have warnings output to the logging facility instead of stderr.
- If the number of pixels is greater than twice :py:data:`PIL.Image.MAX_IMAGE_PIXELS`, then a
+ If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a
``DecompressionBombError`` will be raised instead.
.. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb
@@ -255,7 +255,7 @@ This rotates the input image by ``theta`` degrees counter clockwise:
.. automethod:: PIL.Image.Image.transform
.. automethod:: PIL.Image.Image.transpose
-This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`
+This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT`
method.
.. code-block:: python
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index b95d8d591..25f98b767 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -64,7 +64,7 @@ Fonts
PIL can use bitmap fonts or OpenType/TrueType fonts.
-Bitmap fonts are stored in PIL’s own format, where each font typically consists
+Bitmap fonts are stored in PIL's own format, where each font typically consists
of two files, one named .pil and the other usually named .pbm. The former
contains font metrics, the latter raster data.
@@ -146,6 +146,11 @@ Methods
Get the current default font.
+ To set the default font for all future ImageDraw instances::
+
+ from PIL import ImageDraw, ImageFont
+ ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
:returns: An image font.
.. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0)
@@ -280,8 +285,8 @@ Methods
Draws a rectangle.
:param xy: Two points to define the bounding box. Sequence of either
- ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point
- is just outside the drawn rectangle.
+ ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box
+ is inclusive of both endpoints.
:param outline: Color to use for the outline.
:param fill: Color to use for the fill.
:param width: The line width, in pixels.
@@ -293,8 +298,8 @@ Methods
Draws a rounded rectangle.
:param xy: Two points to define the bounding box. Sequence of either
- ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The second point
- is just outside the drawn rectangle.
+ ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box
+ is inclusive of both endpoints.
:param radius: Radius of the corners.
:param outline: Color to use for the outline.
:param fill: Color to use for the fill.
@@ -436,12 +441,16 @@ Methods
.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
- Return the size of the given string, in pixels.
+ .. deprecated:: 9.2.0
+
+ See :ref:`deprecations ` for more information.
Use :py:meth:`textlength()` to measure the offset of following text with
1/64 pixel precision.
Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor.
+ Return the size of the given string, in pixels.
+
.. note:: For historical reasons this function measures text height from
the ascender line instead of the top, see :ref:`text-anchors`.
If you wish to measure text height from the top, it is recommended
@@ -482,8 +491,16 @@ Methods
.. versionadded:: 6.2.0
+ :return: (width, height)
+
.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
+ .. deprecated:: 9.2.0
+
+ See :ref:`deprecations ` for more information.
+
+ Use :py:meth:`.multiline_textbbox` instead.
+
Return the size of the given string, in pixels.
Use :py:meth:`textlength()` to measure the offset of following text with
@@ -530,6 +547,8 @@ Methods
.. versionadded:: 6.2.0
+ :return: (width, height)
+
.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)
Returns length (in pixels with 1/64 precision) of given text when rendered
@@ -597,6 +616,7 @@ Methods
It should be a `BCP 47 language code`_.
Requires libraqm.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
+ :return: Width for horizontal, height for vertical text.
.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
@@ -646,6 +666,7 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
+ :return: ``(left, top, right, bottom)`` bounding box
.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
@@ -689,6 +710,7 @@ Methods
Requires libraqm.
:param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
+ :return: ``(left, top, right, bottom)`` bounding box
.. py:method:: getdraw(im=None, hints=None)
@@ -720,4 +742,4 @@ Methods
homogeneous, but similar, colors.
.. _BCP 47 language code: https://www.w3.org/International/articles/language-tags/
-.. _OpenType docs: https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+.. _OpenType docs: https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 8efef7cfd..516fa63a7 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -56,6 +56,7 @@ Methods
.. autoclass:: PIL.ImageFont.TransposedFont
:members:
+ :undoc-members:
Constants
---------
diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst
index ac83b2255..3086ba8c3 100644
--- a/docs/reference/ImageGrab.rst
+++ b/docs/reference/ImageGrab.rst
@@ -15,7 +15,10 @@ or the clipboard to a PIL image memory.
returned as an "RGBA" on macOS, or an "RGB" image otherwise.
If the bounding box is omitted, the entire screen is copied.
- .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux (X11))
+ On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it
+ is installed. To capture the default X11 display instead, pass ``xdisplay=""``.
+
+ .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
:param bbox: What region to copy. Default is the entire screen.
Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used.
diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst
index d2e80fb8c..b234b7b4e 100644
--- a/docs/reference/PixelAccess.rst
+++ b/docs/reference/PixelAccess.rst
@@ -73,7 +73,7 @@ Access using negative indexes is also possible.
Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples
- are accepted for P images.
+ are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y).
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)
diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst
index 75e8da655..d9b8f0fb7 100644
--- a/docs/releasenotes/5.2.0.rst
+++ b/docs/releasenotes/5.2.0.rst
@@ -105,7 +105,7 @@ Resolve confusion getting PIL / Pillow version string
Re: "version constants deprecated" listed above, as user gnbl notes in #3082:
- it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's
-- there does not seem to be documentation on this version number (why this, will it ever change, ..) e.g. at https://pillow.readthedocs.io/en/5.1.x/about.html#why-a-fork
+- ReadTheDocs documentation is missing for some version branches (why is this, will it ever change, ...)
- it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it
- the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version)
- PIL._version module documentation comment could explain how to access the version information
diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst
index dd993d39e..a19da361a 100644
--- a/docs/releasenotes/9.0.0.rst
+++ b/docs/releasenotes/9.0.0.rst
@@ -45,7 +45,7 @@ Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported
We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe
vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
-.. _FreeType: https://www.freetype.org
+.. _FreeType: https://freetype.org/
Image.show command parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst
index 424fd487a..6dbfa2702 100644
--- a/docs/releasenotes/9.2.0.rst
+++ b/docs/releasenotes/9.2.0.rst
@@ -1,12 +1,6 @@
9.2.0
-----
-Backwards Incompatible Changes
-==============================
-
-TODO
-^^^^
-
Deprecations
============
@@ -46,13 +40,58 @@ Image.coerce_e
This undocumented method has been deprecated and will be removed in Pillow 10
(2023-07-01).
-API Changes
-===========
+Font size and offset methods
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
-^^^^
+.. deprecated:: 9.2.0
-TODO
+Several functions for computing the size and offset of rendered text
+have been deprecated and will be removed in Pillow 10 (2023-07-01):
+
+=========================================================================== =============================================================================================================
+Deprecated Use instead
+=========================================================================== =============================================================================================================
+:py:meth:`.FreeTypeFont.getsize` and :py:meth:`.FreeTypeFont.getoffset` :py:meth:`.FreeTypeFont.getbbox` and :py:meth:`.FreeTypeFont.getlength`
+:py:meth:`.FreeTypeFont.getsize_multiline` :py:meth:`.ImageDraw.multiline_textbbox`
+:py:meth:`.ImageFont.getsize` :py:meth:`.ImageFont.getbbox` and :py:meth:`.ImageFont.getlength`
+:py:meth:`.TransposedFont.getsize` :py:meth:`.TransposedFont.getbbox` and :py:meth:`.TransposedFont.getlength`
+:py:meth:`.ImageDraw.textsize` and :py:meth:`.ImageDraw.multiline_textsize` :py:meth:`.ImageDraw.textbbox`, :py:meth:`.ImageDraw.textlength` and :py:meth:`.ImageDraw.multiline_textbbox`
+:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength`
+=========================================================================== =============================================================================================================
+
+Previous code:
+
+.. code-block:: python
+
+ from PIL import Image, ImageDraw, ImageFont
+
+ font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+ width, height = font.getsize("Hello world")
+ left, top = font.getoffset("Hello world")
+
+ im = Image.new("RGB", (100, 100))
+ draw = ImageDraw.Draw(im)
+ width, height = draw.textsize("Hello world")
+
+ width, height = font.getsize_multiline("Hello\nworld")
+ width, height = draw.multiline_textsize("Hello\nworld")
+
+Use instead:
+
+.. code-block:: python
+
+ from PIL import Image, ImageDraw, ImageFont
+
+ font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+ left, top, right, bottom = font.getbbox("Hello world")
+ width, height = right - left, bottom - top
+
+ im = Image.new("RGB", (100, 100))
+ draw = ImageDraw.Draw(im)
+ width = draw.textlength("Hello world")
+
+ left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld")
+ width, height = right - left, bottom - top
API Additions
=============
@@ -68,15 +107,14 @@ The image's palette mode will become "RGBA", and "transparency" will be removed
Security
========
-TODO
-^^^^
-
-TODO
+An additional decompression bomb check has been added for the GIF format.
Other Changes
=============
-TODO
-^^^^
+Using gnome-screenshot on Linux
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-TODO
+In :py:meth:`~PIL.ImageGrab.grab` on Linux, if ``xdisplay`` is ``None`` then
+``gnome-screenshot`` will be used to capture the display if it is installed. To capture
+the default X11 display instead, pass ``xdisplay=""``.
diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst
new file mode 100644
index 000000000..7109a09f2
--- /dev/null
+++ b/docs/releasenotes/9.3.0.rst
@@ -0,0 +1,69 @@
+9.3.0
+-----
+
+Backwards Incompatible Changes
+==============================
+
+TODO
+^^^^
+
+Deprecations
+============
+
+TODO
+^^^^
+
+TODO
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+Allow default ImageDraw font to be set
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Rather than specifying a font when calling text-related ImageDraw methods, or
+setting a font on each ImageDraw instance, the default font can now be set for
+all future ImageDraw operations::
+
+ from PIL import ImageDraw, ImageFont
+ ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
+Saving multiple MPO frames
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of
+an image's frames will be saved to file::
+
+ from PIL import Image
+ im = Image.open("frozenpond.mpo")
+ im.save(out, save_all=True)
+
+Additional images can also be appended when saving, by combining the
+``save_all`` argument with the ``append_images`` argument::
+
+ im.save(out, save_all=True, append_images=[im1, im2, ...])
+
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+Added DDS ATI1 and ATI2 reading
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read the ATI1 and ATI2 formats of DDS images.
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 597c804f8..8c436be3b 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 9.3.0
9.2.0
9.1.1
9.1.0
diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst
index 87f2ba422..2a0af9e59 100644
--- a/docs/releasenotes/versioning.rst
+++ b/docs/releasenotes/versioning.rst
@@ -11,7 +11,7 @@ Pillow follows `Semantic Versioning `_:
2. MINOR version when you add functionality in a backwards compatible manner, and
3. PATCH version when you make backwards compatible bug fixes.
-Quarterly releases ("`Main Release `_")
+Quarterly releases ("`Main Release `_")
bump at least the MINOR version, as new functionality has likely been added in the
prior three months.
@@ -21,8 +21,8 @@ these occur every 12-18 months, guided by
`Python's EOL schedule `_, and
any APIs that have been deprecated for at least a year are removed at the same time.
-PATCH versions ("`Point Release `_"
-or "`Embargoed Release `_")
+PATCH versions ("`Point Release `_"
+or "`Embargoed Release `_")
are for security, installation or critical bug fixes. These are less common as it is
preferred to stick to quarterly releases.
diff --git a/setup.cfg b/setup.cfg
index 82873fce9..44feb25ff 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,6 +16,7 @@ classifiers =
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
+ Programming Language :: Python :: 3.11
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Multimedia :: Graphics
@@ -33,7 +34,11 @@ project_urls =
Twitter=https://twitter.com/PythonPillow
[options]
+packages = PIL
python_requires = >=3.7
+include_package_data = True
+package_dir =
+ = src
[options.extras_require]
docs =
diff --git a/setup.py b/setup.py
index 2701aa1ce..aa3168aa5 100755
--- a/setup.py
+++ b/setup.py
@@ -15,7 +15,9 @@ import subprocess
import sys
import warnings
-from setuptools import Extension, setup
+from setuptools import Extension
+from setuptools import __version__ as setuptools_version
+from setuptools import setup
from setuptools.command.build_ext import build_ext
@@ -38,7 +40,7 @@ TIFF_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
-if sys.platform == "win32" and sys.version_info >= (3, 11):
+if sys.platform == "win32" and sys.version_info >= (3, 12):
import atexit
atexit.register(
@@ -850,6 +852,7 @@ class pil_build_ext(build_ext):
sys.platform == "win32"
and sys.version_info < (3, 9)
and not (PLATFORM_PYPY or PLATFORM_MINGW)
+ and int(setuptools_version.split(".")[0]) < 60
):
defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""'))
else:
@@ -996,9 +999,6 @@ try:
version=PILLOW_VERSION,
cmdclass={"build_ext": pil_build_ext},
ext_modules=ext_modules,
- include_package_data=True,
- packages=["PIL"],
- package_dir={"": "src"},
zip_safe=not (debug_build() or PLATFORM_MINGW),
)
except RequiredDependencyException as err:
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 4dc2b93c3..1041ab763 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -172,10 +172,11 @@ class BmpImageFile(ImageFile.ImageFile):
SUPPORTED = {
32: [
(0xFF0000, 0xFF00, 0xFF, 0x0),
- (0xFF0000, 0xFF00, 0xFF, 0xFF000000),
- (0xFF, 0xFF00, 0xFF0000, 0xFF000000),
- (0x0, 0x0, 0x0, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
+ (0xFF000000, 0xFF0000, 0xFF00, 0xFF),
+ (0xFF, 0xFF00, 0xFF0000, 0xFF000000),
+ (0xFF0000, 0xFF00, 0xFF, 0xFF000000),
+ (0x0, 0x0, 0x0, 0x0),
],
24: [(0xFF0000, 0xFF00, 0xFF)],
16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
@@ -183,6 +184,7 @@ class BmpImageFile(ImageFile.ImageFile):
MASK_MODES = {
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
+ (32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
@@ -321,7 +323,8 @@ class BmpRleDecoder(ImageFile.PyDecoder):
# align to 16-bit word boundary
if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR)
- self.set_as_raw(bytes(data), ("P", 0, self.args[-1]))
+ rawmode = "L" if self.mode == "L" else "P"
+ self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1]))
return -1, 0
@@ -372,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True):
header = 40 # or 64 for OS/2 version 2
image = stride * im.size[1]
+ if im.mode == "1":
+ palette = b"".join(o8(i) * 4 for i in (0, 255))
+ elif im.mode == "L":
+ palette = b"".join(o8(i) * 4 for i in range(256))
+ elif im.mode == "P":
+ palette = im.im.getpalette("RGB", "BGRX")
+ colors = len(palette) // 4
+ else:
+ palette = None
+
# bitmap header
if bitmap_header:
offset = 14 + header + colors * 4
@@ -402,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True):
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
- if im.mode == "1":
- for i in (0, 255):
- fp.write(o8(i) * 4)
- elif im.mode == "L":
- for i in range(256):
- fp.write(o8(i) * 4)
- elif im.mode == "P":
- fp.write(im.im.getpalette("RGB", "BGRX"))
+ if palette:
+ fp.write(palette)
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index 3a04bdb5d..bba480161 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -156,6 +156,14 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"DXT5":
self.pixel_format = "DXT5"
n = 3
+ elif fourcc == b"ATI1":
+ self.pixel_format = "BC4"
+ n = 4
+ self.mode = "L"
+ elif fourcc == b"ATI2":
+ self.pixel_format = "BC5"
+ n = 5
+ self.mode = "RGB"
elif fourcc == b"BC5S":
self.pixel_format = "BC5S"
n = 5
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 3b782d6b3..0e434c5c0 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -288,11 +288,14 @@ class EpsImageFile(ImageFile.ImageFile):
# Encoded bitmapped image.
x, y, bi, mo = s[11:].split(None, 7)[:4]
- if int(bi) != 8:
- break
- try:
- self.mode = self.mode_map[int(mo)]
- except ValueError:
+ if int(bi) == 1:
+ self.mode = "1"
+ elif int(bi) == 8:
+ try:
+ self.mode = self.mode_map[int(mo)]
+ except ValueError:
+ break
+ else:
break
self._size = int(x), int(y)
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index f955b2347..a55376d0e 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -154,13 +154,16 @@ class FpxImageFile(ImageFile.ImageFile):
for i in range(0, len(s), length):
+ x1 = min(xsize, x + xtile)
+ y1 = min(ysize, y + ytile)
+
compression = i32(s, i + 8)
if compression == 0:
self.tile.append(
(
"raw",
- (x, y, x + xtile, y + ytile),
+ (x, y, x1, y1),
i32(s, i) + 28,
(self.rawmode,),
)
@@ -172,7 +175,7 @@ class FpxImageFile(ImageFile.ImageFile):
self.tile.append(
(
"fill",
- (x, y, x + xtile, y + ytile),
+ (x, y, x1, y1),
i32(s, i) + 28,
(self.rawmode, s[12:16]),
)
@@ -201,7 +204,7 @@ class FpxImageFile(ImageFile.ImageFile):
self.tile.append(
(
"jpeg",
- (x, y, x + xtile, y + ytile),
+ (x, y, x1, y1),
i32(s, i) + 28,
(rawmode, jpegmode),
)
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index db1039a36..60b9b08a0 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -185,8 +185,6 @@ class GifImageFile(ImageFile.ImageFile):
if not s or s == b";":
raise EOFError
- self.tile = []
-
palette = None
info = {}
@@ -265,6 +263,7 @@ class GifImageFile(ImageFile.ImageFile):
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
self._size = max(x1, self.size[0]), max(y1, self.size[1])
+ Image._decompression_bomb_check(self._size)
frame_dispose_extent = x0, y0, x1, y1
flags = s[8]
@@ -294,6 +293,8 @@ class GifImageFile(ImageFile.ImageFile):
if not update_image:
return
+ self.tile = []
+
if self.dispose:
self.im.paste(self.dispose, self.dispose_extent)
@@ -518,9 +519,8 @@ def _normalize_palette(im, palette, info):
used_palette_colors = []
for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3])
- try:
- index = im.palette.colors[source_color]
- except KeyError:
+ index = im.palette.colors.get(source_color)
+ if index in used_palette_colors:
index = None
used_palette_colors.append(index)
for i, index in enumerate(used_palette_colors):
@@ -824,9 +824,18 @@ def _get_optimize(im, info):
if count:
used_palette_colors.append(i)
- if optimise or (
- len(used_palette_colors) <= 128
- and max(used_palette_colors) > len(used_palette_colors)
+ if optimise or max(used_palette_colors) >= len(used_palette_colors):
+ return used_palette_colors
+
+ num_palette_colors = len(im.palette.palette) // Image.getmodebands(
+ im.palette.mode
+ )
+ current_palette_size = 1 << (num_palette_colors - 1).bit_length()
+ if (
+ # check that the palette would become smaller when saved
+ len(used_palette_colors) <= current_palette_size // 2
+ # check that the palette is not already the smallest possible size
+ and current_palette_size > 2
):
return used_palette_colors
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index eb21f03d4..87b180860 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -646,7 +646,7 @@ class Image:
def _repr_pretty_(self, p, cycle):
"""IPython plain text display support"""
- # Same as __repr__ but without unpredicatable id(self),
+ # Same as __repr__ but without unpredictable id(self),
# to keep Jupyter notebook `text/plain` output stable.
p.text(
"<%s.%s image mode=%s size=%dx%d>"
@@ -671,14 +671,9 @@ class Image:
raise ValueError("Could not save to PNG for display") from e
return b.getvalue()
- class _ArrayData:
- def __init__(self, new):
- self.__array_interface__ = new
-
- def __array__(self, dtype=None):
+ @property
+ def __array_interface__(self):
# numpy array interface support
- import numpy as np
-
new = {}
shape, typestr = _conv_type_shape(self)
new["shape"] = shape
@@ -690,8 +685,7 @@ class Image:
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
-
- return np.array(self._ArrayData(new), dtype)
+ return new
def __getstate__(self):
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]
@@ -721,6 +715,11 @@ class Image:
:param encoder_name: What encoder to use. The default is to
use the standard "raw" encoder.
+
+ A list of C encoders can be seen under
+ codecs section of the function array in
+ :file:`_imaging.c`. Python encoders are
+ registered within the relevant plugins.
:param args: Extra arguments to the encoder.
:returns: A :py:class:`bytes` object.
"""
@@ -1405,9 +1404,9 @@ class Image:
if 0x0112 not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags:
- match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags)
+ match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match:
- self._exif[0x0112] = int(match[1])
+ self._exif[0x0112] = int(match[2])
return self._exif
@@ -1840,7 +1839,7 @@ class Image:
Modifies the pixel at the given position. The color is given as
a single numerical value for single-band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples are
- accepted for P images.
+ accepted for P and PA images.
Note that this method is relatively slow. For more extensive changes,
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
@@ -1865,12 +1864,17 @@ class Image:
return self.pyaccess.putpixel(xy, value)
if (
- self.mode == "P"
+ self.mode in ("P", "PA")
and isinstance(value, (list, tuple))
and len(value) in [3, 4]
):
- # RGB or RGBA value for a P image
+ # RGB or RGBA value for a P or PA image
+ if self.mode == "PA":
+ alpha = value[3] if len(value) == 4 else 255
+ value = value[:3]
value = self.palette.getcolor(value, self)
+ if self.mode == "PA":
+ value = (value, alpha)
return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None):
@@ -1945,11 +1949,7 @@ class Image:
m_im = m_im.convert("L")
- # Internally, we require 256 palette entries.
- new_palette_bytes = (
- palette_bytes + ((256 * bands) - len(palette_bytes)) * b"\x00"
- )
- m_im.putpalette(new_palette_bytes, palette_mode)
+ m_im.putpalette(palette_bytes, palette_mode)
m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes)
if "transparency" in self.info:
@@ -1985,18 +1985,14 @@ class Image:
:param size: The requested size in pixels, as a 2-tuple:
(width, height).
:param resample: An optional resampling filter. This can be
- one of :py:data:`PIL.Image.Resampling.NEAREST`,
- :py:data:`PIL.Image.Resampling.BOX`,
- :py:data:`PIL.Image.Resampling.BILINEAR`,
- :py:data:`PIL.Image.Resampling.HAMMING`,
- :py:data:`PIL.Image.Resampling.BICUBIC` or
- :py:data:`PIL.Image.Resampling.LANCZOS`.
+ one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
+ :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
+ :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
If the image has mode "1" or "P", it is always set to
- :py:data:`PIL.Image.Resampling.NEAREST`.
- If the image mode specifies a number of bits, such as "I;16", then the
- default filter is :py:data:`PIL.Image.Resampling.NEAREST`.
- Otherwise, the default filter is
- :py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`.
+ :py:data:`Resampling.NEAREST`. If the image mode specifies a number
+ of bits, such as "I;16", then the default filter is
+ :py:data:`Resampling.NEAREST`. Otherwise, the default filter is
+ :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`.
:param box: An optional 4-tuple of floats providing
the source image region to be scaled.
The values must be within (0, 0, width, height) rectangle.
@@ -2136,12 +2132,12 @@ class Image:
:param angle: In degrees counter clockwise.
:param resample: An optional resampling filter. This can be
- one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
- :py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2
- environment), or :py:data:`PIL.Image.Resampling.BICUBIC`
- (cubic spline interpolation in a 4x4 environment).
- If omitted, or if the image has mode "1" or "P", it is
- set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`.
+ one of :py:data:`Resampling.NEAREST` (use nearest neighbour),
+ :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2
+ environment), or :py:data:`Resampling.BICUBIC` (cubic spline
+ interpolation in a 4x4 environment). If omitted, or if the image has
+ mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`.
+ See :ref:`concept-filters`.
:param expand: Optional expansion flag. If true, expands the output
image to make it large enough to hold the entire rotated image.
If false or omitted, make the output image the same size as the
@@ -2511,14 +2507,11 @@ class Image:
:param size: Requested size.
:param resample: Optional resampling filter. This can be one
- of :py:data:`PIL.Image.Resampling.NEAREST`,
- :py:data:`PIL.Image.Resampling.BOX`,
- :py:data:`PIL.Image.Resampling.BILINEAR`,
- :py:data:`PIL.Image.Resampling.HAMMING`,
- :py:data:`PIL.Image.Resampling.BICUBIC` or
- :py:data:`PIL.Image.Resampling.LANCZOS`.
- If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`.
- (was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0).
+ of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
+ :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
+ :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
+ If omitted, it defaults to :py:data:`Resampling.BICUBIC`.
+ (was :py:data:`Resampling.NEAREST` prior to version 2.5.0).
See: :ref:`concept-filters`.
:param reducing_gap: Apply optimization by resizing the image
in two steps. First, reducing the image by integer times
@@ -2537,29 +2530,41 @@ class Image:
:returns: None
"""
- self.load()
- x, y = map(math.floor, size)
- if x >= self.width and y >= self.height:
- return
+ provided_size = tuple(map(math.floor, size))
- def round_aspect(number, key):
- return max(min(math.floor(number), math.ceil(number), key=key), 1)
+ def preserve_aspect_ratio():
+ def round_aspect(number, key):
+ return max(min(math.floor(number), math.ceil(number), key=key), 1)
- # preserve aspect ratio
- aspect = self.width / self.height
- if x / y >= aspect:
- x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y))
- else:
- y = round_aspect(
- x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n)
- )
- size = (x, y)
+ x, y = provided_size
+ if x >= self.width and y >= self.height:
+ return
+
+ aspect = self.width / self.height
+ if x / y >= aspect:
+ x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y))
+ else:
+ y = round_aspect(
+ x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n)
+ )
+ return x, y
box = None
if reducing_gap is not None:
+ size = preserve_aspect_ratio()
+ if size is None:
+ return
+
res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap))
if res is not None:
box = res[1]
+ if box is None:
+ self.load()
+
+ # load() may have changed the size of the image
+ size = preserve_aspect_ratio()
+ if size is None:
+ return
if self.size != size:
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
@@ -2589,11 +2594,11 @@ class Image:
:param size: The output size.
:param method: The transformation method. This is one of
- :py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion),
- :py:data:`PIL.Image.Transform.AFFINE` (affine transform),
- :py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform),
- :py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or
- :py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals
+ :py:data:`Transform.EXTENT` (cut out a rectangular subregion),
+ :py:data:`Transform.AFFINE` (affine transform),
+ :py:data:`Transform.PERSPECTIVE` (perspective transform),
+ :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or
+ :py:data:`Transform.MESH` (map a number of source quadrilaterals
in one operation).
It may also be an :py:class:`~PIL.Image.ImageTransformHandler`
@@ -2613,11 +2618,11 @@ class Image:
return method, data
:param data: Extra data to the transformation method.
:param resample: Optional resampling filter. It can be one of
- :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
- :py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2
- environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline
+ :py:data:`Resampling.NEAREST` (use nearest neighbour),
+ :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2
+ environment), or :py:data:`Resampling.BICUBIC` (cubic spline
interpolation in a 4x4 environment). If omitted, or if the image
- has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`.
+ has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`.
See: :ref:`concept-filters`.
:param fill: If ``method`` is an
:py:class:`~PIL.Image.ImageTransformHandler` object, this is one of
@@ -2744,13 +2749,10 @@ class Image:
"""
Transpose image (flip or rotate in 90 degree steps)
- :param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`,
- :py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`,
- :py:data:`PIL.Image.Transpose.ROTATE_90`,
- :py:data:`PIL.Image.Transpose.ROTATE_180`,
- :py:data:`PIL.Image.Transpose.ROTATE_270`,
- :py:data:`PIL.Image.Transpose.TRANSPOSE` or
- :py:data:`PIL.Image.Transpose.TRANSVERSE`.
+ :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`,
+ :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`,
+ :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`,
+ :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`.
:returns: Returns a flipped or rotated copy of this image.
"""
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 50ec3b5ef..605252d5d 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -377,7 +377,7 @@ def profileToProfile(
raise PyCMSError("renderingIntent must be an integer between 0 and 3")
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+ raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}")
try:
if not isinstance(inputProfile, ImageCmsProfile):
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index 3824626bd..e84dafb12 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -32,8 +32,10 @@
import math
import numbers
+import warnings
from . import Image, ImageColor
+from ._deprecate import deprecate
"""
A simple 2D drawing interface for PIL images.
@@ -44,6 +46,8 @@ directly.
class ImageDraw:
+ font = None
+
def __init__(self, im, mode=None):
"""
Create a drawing instance.
@@ -84,12 +88,16 @@ class ImageDraw:
else:
self.fontmode = "L" # aliasing is okay for other modes
self.fill = 0
- self.font = None
def getfont(self):
"""
Get the current default font.
+ To set the default font for all future ImageDraw instances::
+
+ from PIL import ImageDraw, ImageFont
+ ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
:returns: An image font."""
if not self.font:
# FIXME: should add a font repository
@@ -372,6 +380,19 @@ class ImageDraw:
return text.split(split_character)
+ def _multiline_spacing(self, font, spacing, stroke_width):
+ # this can be replaced with self.textbbox(...)[3] when textsize is removed
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ return (
+ self.textsize(
+ "A",
+ font=font,
+ stroke_width=stroke_width,
+ )[1]
+ + spacing
+ )
+
def text(
self,
xy,
@@ -511,9 +532,7 @@ class ImageDraw:
widths = []
max_width = 0
lines = self._multiline_split(text)
- line_spacing = (
- self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
- )
+ line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:
line_width = self.textlength(
line, font, direction=direction, features=features, language=language
@@ -573,14 +592,31 @@ class ImageDraw:
stroke_width=0,
):
"""Get the size of a given string, in pixels."""
+ deprecate("textsize", 10, "textbbox or textlength")
if self._multiline_check(text):
- return self.multiline_textsize(
- text, font, spacing, direction, features, language, stroke_width
- )
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ return self.multiline_textsize(
+ text,
+ font,
+ spacing,
+ direction,
+ features,
+ language,
+ stroke_width,
+ )
if font is None:
font = self.getfont()
- return font.getsize(text, direction, features, language, stroke_width)
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ return font.getsize(
+ text,
+ direction,
+ features,
+ language,
+ stroke_width,
+ )
def multiline_textsize(
self,
@@ -592,16 +628,23 @@ class ImageDraw:
language=None,
stroke_width=0,
):
+ deprecate("multiline_textsize", 10, "multiline_textbbox")
max_width = 0
lines = self._multiline_split(text)
- line_spacing = (
- self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
- )
- for line in lines:
- line_width, line_height = self.textsize(
- line, font, spacing, direction, features, language, stroke_width
- )
- max_width = max(max_width, line_width)
+ line_spacing = self._multiline_spacing(font, spacing, stroke_width)
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ for line in lines:
+ line_width, line_height = self.textsize(
+ line,
+ font,
+ spacing,
+ direction,
+ features,
+ language,
+ stroke_width,
+ )
+ max_width = max(max_width, line_width)
return max_width, len(lines) * line_spacing - spacing
def textlength(
@@ -625,9 +668,16 @@ class ImageDraw:
try:
return font.getlength(text, mode, direction, features, language)
except AttributeError:
- size = self.textsize(
- text, font, direction=direction, features=features, language=language
- )
+ deprecate("textlength support for fonts without getlength", 10)
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ size = self.textsize(
+ text,
+ font,
+ direction=direction,
+ features=features,
+ language=language,
+ )
if direction == "ttb":
return size[1]
return size[0]
@@ -667,10 +717,6 @@ class ImageDraw:
if font is None:
font = self.getfont()
- from . import ImageFont
-
- if not isinstance(font, ImageFont.FreeTypeFont):
- raise ValueError("Only supported for TrueType fonts")
mode = "RGBA" if embedded_color else self.fontmode
bbox = font.getbbox(
text, mode, direction, features, language, stroke_width, anchor
@@ -704,9 +750,7 @@ class ImageDraw:
widths = []
max_width = 0
lines = self._multiline_split(text)
- line_spacing = (
- self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
- )
+ line_spacing = self._multiline_spacing(font, spacing, stroke_width)
for line in lines:
line_width = self.textlength(
line,
diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py
index 1f63110fd..2667b77dd 100644
--- a/src/PIL/ImageDraw2.py
+++ b/src/PIL/ImageDraw2.py
@@ -24,7 +24,10 @@
"""
+import warnings
+
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
+from ._deprecate import deprecate
class Pen:
@@ -172,8 +175,35 @@ class Draw:
def textsize(self, text, font):
"""
+ .. deprecated:: 9.2.0
+
Return the size of the given string, in pixels.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize`
"""
- return self.draw.textsize(text, font=font.font)
+ deprecate("textsize", 10, "textbbox or textlength")
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ return self.draw.textsize(text, font=font.font)
+
+ def textbbox(self, xy, text, font):
+ """
+ Returns bounding box (in pixels) of given text.
+
+ :return: ``(left, top, right, bottom)`` bounding box
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox`
+ """
+ if self.transform:
+ xy = ImagePath.Path(xy)
+ xy.transform(self.transform)
+ return self.draw.textbbox(xy, text, font=font.font)
+
+ def textlength(self, text, font):
+ """
+ Returns length (in pixels) of given text.
+ This is the amount by which following text should be offset.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength`
+ """
+ return self.draw.textlength(text, font=font.font)
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index 99b77a37f..f281b9e14 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -192,6 +192,9 @@ class ImageFile(Image.Image):
with open(self.filename) as fp:
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
+ if offset + self.size[1] * args[1] > self.map.size():
+ # buffer is not large enough
+ raise OSError
self.im = Image.core.map_buffer(
self.map, self.size, decoder_name, offset, args
)
@@ -499,9 +502,14 @@ def _save(im, fp, tile, bufsize=0):
try:
fh = fp.fileno()
fp.flush()
- exc = None
- except (AttributeError, io.UnsupportedOperation) as e:
- exc = e
+ _encode_tile(im, fp, tile, bufsize, fh)
+ except (AttributeError, io.UnsupportedOperation) as exc:
+ _encode_tile(im, fp, tile, bufsize, None, exc)
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
for e, b, o, a in tile:
if o > 0:
fp.seek(o)
@@ -526,8 +534,6 @@ def _save(im, fp, tile, bufsize=0):
raise OSError(f"encoder error {s} when writing image file") from exc
finally:
encoder.cleanup()
- if hasattr(fp, "flush"):
- fp.flush()
def _safe_read(fp, size):
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 681b75d44..8be7f0f10 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -137,12 +137,19 @@ class ImageFont:
def getsize(self, text, *args, **kwargs):
"""
+ .. deprecated:: 9.2.0
+
+ Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead.
+
+ See :ref:`deprecations ` for more information.
+
Returns width and height (in pixels) of given text.
:param text: Text to measure.
:return: (width, height)
"""
+ deprecate("getsize", 10, "getbbox or getlength")
return self.font.getsize(text)
def getmask(self, text, mode="", *args, **kwargs):
@@ -165,6 +172,33 @@ class ImageFont:
"""
return self.font.getmask(text, mode)
+ def getbbox(self, text, *args, **kwargs):
+ """
+ Returns bounding box (in pixels) of given text.
+
+ .. versionadded:: 9.2.0
+
+ :param text: Text to render.
+ :param mode: Used by some graphics drivers to indicate what mode the
+ driver prefers; if empty, the renderer may return either
+ mode. Note that the mode is always a string, to simplify
+ C-level implementations.
+
+ :return: ``(left, top, right, bottom)`` bounding box
+ """
+ width, height = self.font.getsize(text)
+ return 0, 0, width, height
+
+ def getlength(self, text, *args, **kwargs):
+ """
+ Returns length (in pixels) of given text.
+ This is the amount by which following text should be offset.
+
+ .. versionadded:: 9.2.0
+ """
+ width, height = self.font.getsize(text)
+ return width
+
##
# Wrapper for FreeType fonts. Application code should use the
@@ -306,7 +340,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
- https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
@@ -359,7 +393,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
- https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
@@ -386,16 +420,25 @@ class FreeTypeFont:
return left, top, left + width, top + height
def getsize(
- self, text, direction=None, features=None, language=None, stroke_width=0
+ self,
+ text,
+ direction=None,
+ features=None,
+ language=None,
+ stroke_width=0,
):
"""
- Returns width and height (in pixels) of given text if rendered in font with
- provided direction, features, and language.
+ .. deprecated:: 9.2.0
Use :py:meth:`getlength()` to measure the offset of following text with
1/64 pixel precision.
Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor.
+ See :ref:`deprecations ` for more information.
+
+ Returns width and height (in pixels) of given text if rendered in font with
+ provided direction, features, and language.
+
.. note:: For historical reasons this function measures text height from
the ascender line instead of the top, see :ref:`text-anchors`.
If you wish to measure text height from the top, it is recommended
@@ -417,7 +460,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
- https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
@@ -438,6 +481,7 @@ class FreeTypeFont:
:return: (width, height)
"""
+ deprecate("getsize", 10, "getbbox or getlength")
# vertical offset is added for historical reasons
# see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929
size, offset = self.font.getsize(text, "L", direction, features, language)
@@ -456,6 +500,12 @@ class FreeTypeFont:
stroke_width=0,
):
"""
+ .. deprecated:: 9.2.0
+
+ Use :py:meth:`.ImageDraw.multiline_textbbox` instead.
+
+ See :ref:`deprecations ` for more information.
+
Returns width and height (in pixels) of given text if rendered in font
with provided direction, features, and language, while respecting
newline characters.
@@ -476,7 +526,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
- https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
:param language: Language of the text. Different languages may use
@@ -495,19 +545,28 @@ class FreeTypeFont:
:return: (width, height)
"""
+ deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox")
max_width = 0
lines = self._multiline_split(text)
- line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing
- for line in lines:
- line_width, line_height = self.getsize(
- line, direction, features, language, stroke_width
- )
- max_width = max(max_width, line_width)
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing
+ for line in lines:
+ line_width, line_height = self.getsize(
+ line, direction, features, language, stroke_width
+ )
+ max_width = max(max_width, line_width)
return max_width, len(lines) * line_spacing - spacing
def getoffset(self, text):
"""
+ .. deprecated:: 9.2.0
+
+ Use :py:meth:`.getbbox` instead.
+
+ See :ref:`deprecations ` for more information.
+
Returns the offset of given text. This is the gap between the
starting coordinate and the first marking. Note that this gap is
included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`.
@@ -516,6 +575,7 @@ class FreeTypeFont:
:return: A tuple of the x and y offset
"""
+ deprecate("getoffset", 10, "getbbox")
return self.font.getsize(text)[1]
def getmask(
@@ -558,7 +618,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
- https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
@@ -650,7 +710,7 @@ class FreeTypeFont:
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
- https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
@@ -796,7 +856,17 @@ class TransposedFont:
self.orientation = orientation # any 'transpose' argument, or None
def getsize(self, text, *args, **kwargs):
- w, h = self.font.getsize(text)
+ """
+ .. deprecated:: 9.2.0
+
+ Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead.
+
+ See :ref:`deprecations ` for more information.
+ """
+ deprecate("getsize", 10, "getbbox or getlength")
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ w, h = self.font.getsize(text)
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
return h, w
return w, h
@@ -807,6 +877,23 @@ class TransposedFont:
return im.transpose(self.orientation)
return im
+ def getbbox(self, text, *args, **kwargs):
+ # TransposedFont doesn't support getmask2, move top-left point to (0, 0)
+ # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont
+ left, top, right, bottom = self.font.getbbox(text, *args, **kwargs)
+ width = right - left
+ height = bottom - top
+ if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
+ return 0, 0, height, width
+ return 0, 0, width, height
+
+ def getlength(self, text, *args, **kwargs):
+ if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
+ raise ValueError(
+ "text length is undefined for text rotated by 90 or 270 degrees"
+ )
+ return self.font.getlength(text, *args, **kwargs)
+
def load(filename):
"""
@@ -829,10 +916,12 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
This function loads a font object from the given file or file-like
object, and creates a font object for a font of the given size.
- Pillow uses FreeType to open font files. If you are opening many fonts
- simultaneously on Windows, be aware that Windows limits the number of files
- that can be open in C at once to 512. If you approach that limit, an
+ Pillow uses FreeType to open font files. On Windows, be aware that FreeType
+ will keep the file open as long as the FreeTypeFont object exists. Windows
+ limits the number of files that can be open in C at once to 512, so if many
+ fonts are opened simultaneously and that limit is approached, an
``OSError`` may be thrown, reporting that FreeType "cannot open resource".
+ A workaround would be to copy the file(s) into memory, and open that instead.
This function requires the _imagingft service.
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index eb21ac399..38074cb1b 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -15,15 +15,14 @@
# See the README file for information on usage and redistribution.
#
+import os
+import shutil
+import subprocess
import sys
+import tempfile
from . import Image
-if sys.platform == "darwin":
- import os
- import subprocess
- import tempfile
-
def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):
if xdisplay is None:
@@ -62,6 +61,18 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im
+ elif shutil.which("gnome-screenshot"):
+ fh, filepath = tempfile.mkstemp(".png")
+ os.close(fh)
+ subprocess.call(["gnome-screenshot", "-f", filepath])
+ im = Image.open(filepath)
+ im.load()
+ os.unlink(filepath)
+ if bbox:
+ im_cropped = im.crop(bbox)
+ im.close()
+ return im_cropped
+ return im
# use xdisplay=None for default display on non-win32/macOS systems
if not Image.core.HAVE_XCB:
raise OSError("Pillow was built without XCB support")
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index f0d4545ba..443c540b6 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -21,7 +21,7 @@ import functools
import operator
import re
-from . import Image
+from . import Image, ImagePalette
#
# helpers
@@ -255,11 +255,11 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
if im_ratio != dest_ratio:
if im_ratio > dest_ratio:
- new_height = int(image.height / image.width * size[0])
+ new_height = round(image.height / image.width * size[0])
if new_height != size[1]:
size = (size[0], new_height)
else:
- new_width = int(image.width / image.height * size[1])
+ new_width = round(image.width / image.height * size[1])
if new_width != size[0]:
size = (new_width, size[1])
return image.resize(size, resample=method)
@@ -291,11 +291,13 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5
out = resized
else:
out = Image.new(image.mode, size, color)
+ if resized.palette:
+ out.putpalette(resized.getpalette())
if resized.width != size[0]:
- x = int((size[0] - resized.width) * max(0, min(centering[0], 1)))
+ x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
out.paste(resized, (x, 0))
else:
- y = int((size[1] - resized.height) * max(0, min(centering[1], 1)))
+ y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
out.paste(resized, (0, y))
return out
@@ -396,9 +398,8 @@ def expand(image, border=0, fill=0):
width = left + image.size[0] + right
height = top + image.size[1] + bottom
color = _color(fill, image.mode)
- if image.mode == "P" and image.palette:
- image.load()
- palette = image.palette.copy()
+ if image.palette:
+ palette = ImagePalette.ImagePalette(palette=image.getpalette())
if isinstance(color, tuple):
color = palette.getcolor(color)
else:
@@ -572,8 +573,11 @@ def solarize(image, threshold=128):
def exif_transpose(image):
"""
- If an image has an EXIF Orientation tag, return a new image that is
- transposed accordingly. Otherwise, return a copy of the image.
+ If an image has an EXIF Orientation tag, other than 1, return a new image
+ that is transposed accordingly. The new image will have the orientation
+ data removed.
+
+ Otherwise, return a copy of the image.
:param image: The image to transpose.
:return: An image.
@@ -601,10 +605,12 @@ def exif_transpose(image):
"Raw profile type exif"
] = transposed_exif.tobytes().hex()
elif "XML:com.adobe.xmp" in transposed_image.info:
- transposed_image.info["XML:com.adobe.xmp"] = re.sub(
+ for pattern in (
r'tiff:Orientation="([0-9])"',
- "",
- transposed_image.info["XML:com.adobe.xmp"],
- )
+ r"([0-9])",
+ ):
+ transposed_image.info["XML:com.adobe.xmp"] = re.sub(
+ pattern, "", transposed_image.info["XML:com.adobe.xmp"]
+ )
return transposed_image
return image.copy()
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index 853147ac2..b73b2cd9d 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -50,15 +50,24 @@ class ImagePalette:
@palette.setter
def palette(self, palette):
+ self._colors = None
self._palette = palette
- mode_len = len(self.mode)
- self.colors = {}
- for i in range(0, len(self.palette), mode_len):
- color = tuple(self.palette[i : i + mode_len])
- if color in self.colors:
- continue
- self.colors[color] = i // mode_len
+ @property
+ def colors(self):
+ if self._colors is None:
+ mode_len = len(self.mode)
+ self._colors = {}
+ for i in range(0, len(self.palette), mode_len):
+ color = tuple(self.palette[i : i + mode_len])
+ if color in self._colors:
+ continue
+ self._colors[color] = i // mode_len
+ return self._colors
+
+ @colors.setter
+ def colors(self, colors):
+ self._colors = colors
def copy(self):
new = ImagePalette()
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 9117f57e5..9f9a551fb 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -178,14 +178,16 @@ class MacViewer(Viewer):
else:
raise TypeError("Missing required argument: 'path'")
subprocess.call(["open", "-a", "Preview.app", path])
- subprocess.Popen(
- [
- sys.executable,
- "-c",
- "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])",
- path,
- ]
- )
+ executable = sys.executable or shutil.which("python3")
+ if executable:
+ subprocess.Popen(
+ [
+ executable,
+ "-c",
+ "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])",
+ path,
+ ]
+ )
return 1
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index c2c4d774c..949cf1fbf 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -68,21 +68,7 @@ def _pyimagingtkcall(command, photo, id):
# may raise an error if it cannot attach to Tkinter
from . import _imagingtk
- try:
- if hasattr(tk, "interp"):
- # Required for PyPy, which always has CFFI installed
- from cffi import FFI
-
- ffi = FFI()
-
- # PyPy is using an FFI CDATA element
- # (Pdb) self.tk.interp
- #
- _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
- else:
- _imagingtk.tkinit(tk.interpaddr(), 1)
- except AttributeError:
- _imagingtk.tkinit(id(tk), 0)
+ _imagingtk.tkinit(tk.interpaddr())
tk.call(command, photo, id)
@@ -121,6 +107,7 @@ class PhotoImage:
mode = image.mode
if mode == "P":
# palette mapped data
+ image.apply_transparency()
image.load()
try:
mode = image.palette.mode
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 23b38119a..448c88ea1 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -711,7 +711,7 @@ def _save(im, fp, filename):
qtables = getattr(im, "quantization", None)
qtables = validate_qtables(qtables)
- extra = b""
+ extra = info.get("extra", b"")
icc_profile = info.get("icc_profile")
if icc_profile:
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 27c30958c..5bfd8efc1 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -18,16 +18,66 @@
# See the README file for information on usage and redistribution.
#
-from . import Image, ImageFile, JpegImagePlugin
+import itertools
+import os
+import struct
+
+from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
from ._binary import i16be as i16
+from ._binary import o32le
# def _accept(prefix):
# return JpegImagePlugin._accept(prefix)
def _save(im, fp, filename):
- # Note that we can only save the current frame at present
- return JpegImagePlugin._save(im, fp, filename)
+ JpegImagePlugin._save(im, fp, filename)
+
+
+def _save_all(im, fp, filename):
+ append_images = im.encoderinfo.get("append_images", [])
+ if not append_images:
+ try:
+ animated = im.is_animated
+ except AttributeError:
+ animated = False
+ if not animated:
+ _save(im, fp, filename)
+ return
+
+ offsets = []
+ for imSequence in itertools.chain([im], append_images):
+ for im_frame in ImageSequence.Iterator(imSequence):
+ if not offsets:
+ # APP2 marker
+ im.encoderinfo["extra"] = (
+ b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70
+ )
+ JpegImagePlugin._save(im_frame, fp, filename)
+ offsets.append(fp.tell())
+ else:
+ im_frame.save(fp, "JPEG")
+ offsets.append(fp.tell() - offsets[-1])
+
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[0xB001] = len(offsets)
+
+ mpentries = b""
+ data_offset = 0
+ for i, size in enumerate(offsets):
+ if i == 0:
+ mptype = 0x030000 # Baseline MP Primary Image
+ else:
+ mptype = 0x000000 # Undefined
+ mpentries += struct.pack(" psd_channels:
raise OSError("not enough channels")
+ if mode == "RGB" and psd_channels == 4:
+ mode = "RGBA"
+ channels = 4
self.mode = mode
self._size = i32(s, 18), i32(s, 14)
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
index 2a48c53f7..9a2ec48fc 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -58,7 +58,7 @@ class PyAccess:
# Keep pointer to im object to prevent dereferencing.
self._im = img.im
- if self._im.mode == "P":
+ if self._im.mode in ("P", "PA"):
self._palette = img.palette
# Debugging is polluting test traces, only useful here
@@ -89,12 +89,17 @@ class PyAccess:
(x, y) = self.check_xy((x, y))
if (
- self._im.mode == "P"
+ self._im.mode in ("P", "PA")
and isinstance(color, (list, tuple))
and len(color) in [3, 4]
):
- # RGB or RGBA value for a P image
+ # RGB or RGBA value for a P or PA image
+ if self._im.mode == "PA":
+ alpha = color[3] if len(color) == 4 else 255
+ color = color[:3]
color = self._palette.getcolor(color, self._img)
+ if self._im.mode == "PA":
+ color = (color, alpha)
return self.set_pixel(x, y, color)
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 59b89e988..cd454b755 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -193,9 +193,10 @@ def _save(im, fp, filename):
warnings.warn("id_section has been trimmed to 255 characters")
if colormaptype:
- colormapfirst, colormaplength, colormapentry = 0, 256, 24
+ palette = im.im.getpalette("RGB", "BGR")
+ colormaplength, colormapentry = len(palette) // 3, 24
else:
- colormapfirst, colormaplength, colormapentry = 0, 0, 0
+ colormaplength, colormapentry = 0, 0
if im.mode in ("LA", "RGBA"):
flags = 8
@@ -210,7 +211,7 @@ def _save(im, fp, filename):
o8(id_len)
+ o8(colormaptype)
+ o8(imagetype)
- + o16(colormapfirst)
+ + o16(0) # colormapfirst
+ o16(colormaplength)
+ o8(colormapentry)
+ o16(0)
@@ -225,7 +226,7 @@ def _save(im, fp, filename):
fp.write(id_section)
if colormaptype:
- fp.write(im.im.getpalette("RGB", "BGR"))
+ fp.write(palette)
if rle:
ImageFile._save(
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 0dd49340d..766d46ffb 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -727,7 +727,9 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(2)
def write_string(self, value):
# remerge of https://github.com/python-pillow/Pillow/pull/1416
- return b"" + value.encode("ascii", "replace") + b"\0"
+ if not isinstance(value, bytes):
+ value = value.encode("ascii", "replace")
+ return value + b"\0"
@_register_loader(5, 8)
def load_rational(self, data, legacy_api=True):
@@ -1146,6 +1148,39 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
+ def get_child_images(self):
+ if SUBIFD not in self.tag_v2:
+ return []
+ child_images = []
+ exif = self.getexif()
+ offset = None
+ for im_offset in self.tag_v2[SUBIFD]:
+ # reset buffered io handle in case fp
+ # was passed to libtiff, invalidating the buffer
+ current_offset = self._fp.tell()
+ if offset is None:
+ offset = current_offset
+
+ fp = self._fp
+ ifd = exif._get_ifd_dict(im_offset)
+ jpegInterchangeFormat = ifd.get(513)
+ if jpegInterchangeFormat is not None:
+ fp.seek(jpegInterchangeFormat)
+ jpeg_data = fp.read(ifd.get(514))
+
+ fp = io.BytesIO(jpeg_data)
+
+ with Image.open(fp) as im:
+ if jpegInterchangeFormat is None:
+ im._frame_pos = [im_offset]
+ im._seek(0)
+ im.load()
+ child_images.append(im)
+
+ if offset is not None:
+ self._fp.seek(offset)
+ return child_images
+
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
@@ -1153,7 +1188,7 @@ class TiffImageFile(ImageFile.ImageFile):
:returns: XMP tags in a dictionary.
"""
- return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {}
+ return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}
def get_photoshop_blocks(self):
"""
@@ -1328,7 +1363,7 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug(f"- photometric_interpretation: {photo}")
logger.debug(f"- planar_configuration: {self._planar_configuration}")
logger.debug(f"- fill_order: {fillorder}")
- logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}")
+ logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")
# size
xsize = int(self.tag_v2.get(IMAGEWIDTH))
@@ -1469,8 +1504,8 @@ class TiffImageFile(ImageFile.ImageFile):
else:
# tiled image
offsets = self.tag_v2[TILEOFFSETS]
- w = self.tag_v2.get(322)
- h = self.tag_v2.get(323)
+ w = self.tag_v2.get(TILEWIDTH)
+ h = self.tag_v2.get(TILELENGTH)
for offset in offsets:
if x + w > xsize:
@@ -1684,7 +1719,8 @@ def _save(im, fp, filename):
stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
# aim for given strip size (64 KB by default) when using libtiff writer
if libtiff:
- rows_per_strip = 1 if stride == 0 else min(STRIP_SIZE // stride, im.size[1])
+ im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
+ rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
# JPEG encoder expects multiple of 8 rows
if compression == "jpeg":
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index e3094b4db..3f3a1ccd2 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -160,6 +160,7 @@ TAGS_V2 = {
323: ("TileLength", LONG, 1),
324: ("TileOffsets", LONG, 0),
325: ("TileByteCounts", LONG, 0),
+ 330: ("SubIFDs", LONG, 0),
332: ("InkSet", SHORT, 1),
333: ("InkNames", ASCII, 1),
334: ("NumberOfInks", SHORT, 1),
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 66cb16064..8e736a432 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,2 +1,2 @@
# Master version for Pillow
-__version__ = "9.2.0.dev0"
+__version__ = "9.3.0.dev0"
diff --git a/src/_imagingtk.c b/src/_imagingtk.c
index 3f154166b..b9273b0b8 100644
--- a/src/_imagingtk.c
+++ b/src/_imagingtk.c
@@ -23,33 +23,16 @@ TkImaging_Init(Tcl_Interp *interp);
extern int
load_tkinter_funcs(void);
-/* copied from _tkinter.c (this isn't as bad as it may seem: for new
- versions, we use _tkinter's interpaddr hook instead, and all older
- versions use this structure layout) */
-
-typedef struct {
- PyObject_HEAD Tcl_Interp *interp;
-} TkappObject;
-
static PyObject *
_tkinit(PyObject *self, PyObject *args) {
Tcl_Interp *interp;
PyObject *arg;
- int is_interp;
- if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) {
+ if (!PyArg_ParseTuple(args, "O", &arg)) {
return NULL;
}
- if (is_interp) {
- interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
- } else {
- TkappObject *app;
- /* Do it the hard way. This will break if the TkappObject
- layout changes */
- app = (TkappObject *)PyLong_AsVoidPtr(arg);
- interp = app->interp;
- }
+ interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
/* This will bomb if interp is invalid... */
TkImaging_Init(interp);
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index 5dc17db60..bdc680be4 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -1026,6 +1026,14 @@ pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
}
}
+static void
+pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
+ int x;
+ for (x = 0; x < xsize; x++, in += 4) {
+ *out++ = in[0];
+ }
+}
+
static void
p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
@@ -1209,6 +1217,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
convert = alpha ? pa2l : p2l;
} else if (strcmp(mode, "LA") == 0) {
convert = alpha ? pa2la : p2la;
+ } else if (strcmp(mode, "P") == 0) {
+ convert = pa2p;
} else if (strcmp(mode, "PA") == 0) {
convert = p2pa;
} else if (strcmp(mode, "I") == 0) {
@@ -1233,6 +1243,10 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
if (!imOut) {
return NULL;
}
+ if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) {
+ ImagingPaletteDelete(imOut->palette);
+ imOut->palette = ImagingPaletteDuplicate(imIn->palette);
+ }
ImagingSectionEnter(&cookie);
for (y = 0; y < imIn->ysize; y++) {
diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c
index 20c6bc84b..71a095c2c 100644
--- a/src/libImaging/Palette.c
+++ b/src/libImaging/Palette.c
@@ -200,15 +200,15 @@ ImagingPaletteCacheUpdate(ImagingPalette palette, int r, int g, int b) {
/* Find min and max distances to any point in the box */
r = palette->palette[i * 4 + 0];
- tmin = (r < r0) ? RDIST(r, r1) : (r > r1) ? RDIST(r, r0) : 0;
+ tmin = (r < r0) ? RDIST(r, r0) : (r > r1) ? RDIST(r, r1) : 0;
tmax = (r <= rc) ? RDIST(r, r1) : RDIST(r, r0);
g = palette->palette[i * 4 + 1];
- tmin += (g < g0) ? GDIST(g, g1) : (g > g1) ? GDIST(g, g0) : 0;
+ tmin += (g < g0) ? GDIST(g, g0) : (g > g1) ? GDIST(g, g1) : 0;
tmax += (g <= gc) ? GDIST(g, g1) : GDIST(g, g0);
b = palette->palette[i * 4 + 2];
- tmin += (b < b0) ? BDIST(b, b1) : (b > b1) ? BDIST(b, b0) : 0;
+ tmin += (b < b0) ? BDIST(b, b0) : (b > b1) ? BDIST(b, b1) : 0;
tmax += (b <= bc) ? BDIST(b, b1) : BDIST(b, b0);
dmin[i] = tmin;
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 3bb444c80..7663f96a9 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -24,7 +24,7 @@
*
* This cast is safe, as the top 32-bits of HFILE are guaranteed to be zero,
* see
- * https://docs.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication
+ * https://learn.microsoft.com/en-us/windows/win32/winprog64/interprocess-communication
*/
#ifndef USE_WIN32_FILEIO
#define fd_to_tiff_fd(fd) (fd)
@@ -916,7 +916,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
dump_state(clientstate);
if (state->state == 0) {
- TRACE(("Encoding line bt line"));
+ TRACE(("Encoding line by line"));
while (state->y < state->ysize) {
state->shuffle(
state->buffer,
diff --git a/src/thirdparty/fribidi-shim/fribidi.c b/src/thirdparty/fribidi-shim/fribidi.c
index 04491e17f..5663da86b 100644
--- a/src/thirdparty/fribidi-shim/fribidi.c
+++ b/src/thirdparty/fribidi-shim/fribidi.c
@@ -33,6 +33,7 @@ static void fribidi_get_bracket_types_compat(
int load_fribidi(void) {
int error = 0;
+ const char **p_fribidi_version_info = 0;
p_fribidi = 0;
@@ -87,20 +88,21 @@ int load_fribidi(void) {
LOAD_FUNCTION(fribidi_get_par_embedding_levels);
#ifndef _WIN32
- fribidi_version_info = *(const char**)dlsym(p_fribidi, "fribidi_version_info");
- if (error || (fribidi_version_info == 0)) {
+ p_fribidi_version_info = (const char**)dlsym(p_fribidi, "fribidi_version_info");
+ if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) {
dlclose(p_fribidi);
p_fribidi = 0;
return 2;
}
#else
- fribidi_version_info = *(const char**)GetProcAddress(p_fribidi, "fribidi_version_info");
- if (error || (fribidi_version_info == 0)) {
+ p_fribidi_version_info = (const char**)GetProcAddress(p_fribidi, "fribidi_version_info");
+ if (error || (p_fribidi_version_info == 0) || (*p_fribidi_version_info == 0)) {
FreeLibrary(p_fribidi);
p_fribidi = 0;
return 2;
}
#endif
+ fribidi_version_info = *p_fribidi_version_info;
return 0;
}
diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md
index 02e996e7a..3354a4d25 100644
--- a/src/thirdparty/raqm/README.md
+++ b/src/thirdparty/raqm/README.md
@@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm:
[1]: https://github.com/fribidi/fribidi
[2]: https://github.com/Tehreer/SheenBidi
[3]: https://github.com/harfbuzz/harfbuzz
-[4]: https://www.freetype.org
+[4]: https://freetype.org/
[5]: https://www.gtk.org/gtk-doc
diff --git a/tox.ini b/tox.ini
index 09db05884..21b5d4b50 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,7 +6,7 @@
[tox]
envlist =
lint
- py{37,38,39,310,py3}
+ py{37,38,39,310,311,py3}
minversion = 1.9
[testenv]
diff --git a/winbuild/README.md b/winbuild/README.md
index 611d1ed1a..d8538fbf3 100644
--- a/winbuild/README.md
+++ b/winbuild/README.md
@@ -11,8 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.12 or newer (available as Visual Studio component).
-* Tested on Windows Server 2016 with Visual Studio 2017 Community (AppVeyor).
-* Tested on Windows Server 2019 with Visual Studio 2019 Enterprise (GitHub Actions).
+* Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor).
+* Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
The following is a simplified version of the script used on AppVeyor:
```
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index d7e88ca40..f4515468f 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -108,9 +108,9 @@ header = [
deps = {
"libjpeg": {
"url": SF_PROJECTS
- + "/libjpeg-turbo/files/2.1.3/libjpeg-turbo-2.1.3.tar.gz/download",
- "filename": "libjpeg-turbo-2.1.3.tar.gz",
- "dir": "libjpeg-turbo-2.1.3",
+ + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download",
+ "filename": "libjpeg-turbo-2.1.4.tar.gz",
+ "dir": "libjpeg-turbo-2.1.4",
"build": [
cmd_cmake(
[
@@ -157,9 +157,9 @@ deps = {
# "bins": [r"libtiff\*.dll"],
},
"libwebp": {
- "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.2.tar.gz",
- "filename": "libwebp-1.2.2.tar.gz",
- "dir": "libwebp-1.2.2",
+ "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz",
+ "filename": "libwebp-1.2.4.tar.gz",
+ "dir": "libwebp-1.2.4",
"build": [
cmd_rmdir(r"output\release-static"), # clean
cmd_nmake(
@@ -226,21 +226,21 @@ deps = {
"filename": "lcms2-2.13.1.tar.gz",
"dir": "lcms2-2.13.1",
"patch": {
- r"Projects\VC2019\lcms2_static\lcms2_static.vcxproj": {
+ r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
# default is /MD for x86 and /MT for x64, we need /MD always
"MultiThreaded": "MultiThreadedDLL", # noqa: E501
# retarget to default toolset (selected by vcvarsall.bat)
- "v142": "$(DefaultPlatformToolset)", # noqa: E501
+ "v143": "$(DefaultPlatformToolset)", # noqa: E501
# retarget to latest (selected by vcvarsall.bat)
"10.0": "$(WindowsSDKVersion)", # noqa: E501
}
},
"build": [
cmd_rmdir("Lib"),
- cmd_rmdir(r"Projects\VC2019\Release"),
- cmd_msbuild(r"Projects\VC2019\lcms2.sln", "Release", "Clean"),
+ cmd_rmdir(r"Projects\VC2022\Release"),
+ cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"),
cmd_msbuild(
- r"Projects\VC2019\lcms2.sln", "Release", "lcms2_static:Rebuild"
+ r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild"
),
cmd_xcopy("include", "{inc_dir}"),
],
@@ -281,9 +281,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/4.3.0.zip",
- "filename": "harfbuzz-4.3.0.zip",
- "dir": "harfbuzz-4.3.0",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/5.2.0.zip",
+ "filename": "harfbuzz-5.2.0.zip",
+ "dir": "harfbuzz-5.2.0",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),