Merge branch 'main' into upload-fribidi

This commit is contained in:
Andrew Murray 2022-10-28 21:27:56 +11:00 committed by GitHub
commit b8fc7340d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
146 changed files with 3416 additions and 2453 deletions

View File

@ -25,8 +25,8 @@ install:
- mv c:\pillow-depends-main c:\pillow-depends - mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs9561w32.exe /S - ..\pillow-depends\gs1000w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH% - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\

View File

@ -37,8 +37,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Remove condition when NumPy supports 3.11 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

17
.github/renovate.json vendored Normal file
View File

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

View File

@ -11,6 +11,13 @@ on:
- "**.h" - "**.h"
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
Fuzzing: Fuzzing:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
@ -16,7 +20,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: ~/.cache/pre-commit path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
@ -24,7 +28,7 @@ jobs:
lint-pre-commit- lint-pre-commit-
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.10"
cache: pip cache: pip

View File

@ -14,8 +14,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
# TODO Remove condition when NumPy supports 3.11 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -10,6 +10,10 @@ on:
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
update_release_draft: update_release_draft:
permissions: permissions:

View File

@ -8,6 +8,10 @@ on:
permissions: permissions:
issues: write issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
stale: stale:
if: github.repository_owner == 'python-pillow' if: github.repository_owner == 'python-pillow'
@ -16,7 +20,7 @@ jobs:
steps: steps:
- name: "Check issues" - name: "Check issues"
uses: actions/stale@v5 uses: actions/stale@v6
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action" only-labels: "Awaiting OP Action"

View File

@ -2,6 +2,13 @@ name: Test Cygwin
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
@ -41,7 +48,7 @@ jobs:
qt5-devel-tools subversion xorg-server-extra zlib-devel qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH - name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v1 uses: egor-tensin/cleanup-path@v2
with: with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
@ -79,7 +83,7 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }} MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
@ -73,11 +77,11 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage - name: Upload coverage
run: | uses: codecov/codecov-action@v3
python3 -m pip install codecov with:
bash <(curl -s https://codecov.io/bash) -F GHA_Windows file: ./coverage.xml
env: flags: GHA_Windows
CODECOV_NAME: ${{ matrix.name }} name: ${{ matrix.name }}
success: success:
permissions: permissions:

View File

@ -16,6 +16,10 @@ on:
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:

View File

@ -5,13 +5,17 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
architecture: ["x86", "x64"] architecture: ["x86", "x64"]
include: include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows # PyPy 7.3.4+ only ships 64-bit binaries for Windows
@ -36,7 +40,7 @@ jobs:
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }} architecture: ${{ matrix.architecture }}
@ -55,8 +59,8 @@ jobs:
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
winbuild\depends\gs9561w32.exe /S winbuild\depends\gs1000w32.exe /S
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\ xcopy /S /Y winbuild\depends\test_images\* Tests\images\
@ -66,7 +70,7 @@ jobs:
- name: Cache build - name: Cache build
id: build-cache id: build-cache
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: winbuild\build path: winbuild\build
key: key:
@ -86,19 +90,28 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_zlib.cmd" run: "& winbuild\\build\\build_dep_zlib.cmd"
- name: Build dependencies / LibTiff - name: Build dependencies / xz
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libtiff.cmd" run: "& winbuild\\build\\build_dep_xz.cmd"
- name: Build dependencies / WebP - name: Build dependencies / WebP
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libwebp.cmd" run: "& winbuild\\build\\build_dep_libwebp.cmd"
- name: Build dependencies / LibTiff
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libtiff.cmd"
# for FreeType CBDT/SBIX font support # for FreeType CBDT/SBIX font support
- name: Build dependencies / libpng - name: Build dependencies / libpng
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libpng.cmd" run: "& winbuild\\build\\build_dep_libpng.cmd"
# for FreeType WOFF2 font support
- name: Build dependencies / brotli
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_brotli.cmd"
- name: Build dependencies / FreeType - name: Build dependencies / FreeType
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_freetype.cmd" run: "& winbuild\\build\\build_dep_freetype.cmd"
@ -171,7 +184,7 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
@ -183,6 +196,22 @@ jobs:
run: | run: |
mkdir fribidi\${{ matrix.architecture }} mkdir fribidi\${{ matrix.architecture }}
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }} copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }}
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
rem Skip FriBiDi license, it is not included in the wheel.
set fribidi=!x:~0,7!
if NOT !fribidi!==fribidi (
rem Skip imagequant license, it is not included in the wheel.
set libimagequant=!x:~0,13!
if NOT !libimagequant!==libimagequant (
echo. >> LICENSE
echo ===== %%~nf ===== >> LICENSE
echo. >> LICENSE
type %%f >> LICENSE
)
)
)
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd shell: cmd

View File

@ -5,6 +5,10 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
@ -18,7 +22,7 @@ jobs:
python-version: [ python-version: [
"pypy-3.8", "pypy-3.8",
"pypy-3.7", "pypy-3.7",
"3.11-dev", "3.11",
"3.10", "3.10",
"3.9", "3.9",
"3.8", "3.8",
@ -30,11 +34,6 @@ jobs:
REVERSE: "--reverse" REVERSE: "--reverse"
- python-version: "3.8" - python-version: "3.8"
PYTHONOPTIMIZE: 2 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 }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
@ -43,7 +42,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: pip cache: pip
@ -99,7 +98,6 @@ jobs:
- name: Docs - name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: | run: |
python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
make doccheck make doccheck
- name: After success - name: After success
@ -107,9 +105,11 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }} uses: codecov/codecov-action@v3
env: with:
CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }} file: ./coverage.xml
flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
success: success:
permissions: permissions:

View File

@ -1,4 +1,5 @@
name: Tidelift Align name: Tidelift Align
on: on:
schedule: schedule:
- cron: "30 2 * * *" # daily at 02:30 UTC - cron: "30 2 * * *" # daily at 02:30 UTC
@ -15,6 +16,10 @@ on:
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
if: github.repository_owner == 'python-pillow' if: github.repository_owner == 'python-pillow'

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.6.0 rev: 22.8.0
hooks: hooks:
- id: black - id: black
args: ["--target-version", "py37"] args: ["--target-version", "py37"]
@ -14,18 +14,18 @@ repos:
- id: isort - id: isort
- repo: https://github.com/asottile/yesqa - repo: https://github.com/asottile/yesqa
rev: v1.3.0 rev: v1.4.0
hooks: hooks:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.3.0 rev: v1.3.1
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 5.0.2 rev: 5.0.4
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat] additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
@ -40,6 +40,7 @@ repos:
rev: v4.3.0 rev: v4.3.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json
- id: check-yaml - id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint

View File

@ -5,6 +5,126 @@ Changelog (Pillow)
9.3.0 (unreleased) 9.3.0 (unreleased)
------------------ ------------------
- Fixed set_variation_by_name offset #6445
[radarhere]
- Fix malloc in _imagingft.c:font_setvaraxes #6690
[cgohlke]
- Release Python GIL when converting images using matrix operations #6418
[hmaarrfk]
- Added ExifTags enums #6630
[radarhere]
- Do not modify previous frame when calculating delta in PNG #6683
[radarhere]
- Added support for reading BMP images with RLE4 compression #6674
[npjg, radarhere]
- Decode JPEG compressed BLP1 data in original mode #6678
[radarhere]
- Added GPS TIFF tag info #6661
[radarhere]
- Added conversion between RGB/RGBA/RGBX and LAB #6647
[radarhere]
- Do not attempt normalization if mode is already normal #6644
[radarhere]
- Fixed seeking to an L frame in a GIF #6576
[radarhere]
- Consider all frames when selecting mode for PNG save_all #6610
[radarhere]
- Don't reassign crc on ChunkStream close #6627
[wiredfool, radarhere]
- Raise a warning if NumPy failed to raise an error during conversion #6594
[radarhere]
- Show all frames in ImageShow #6611
[radarhere]
- Allow FLI palette chunk to not be first #6626
[radarhere]
- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592
[radarhere]
- Round box position to integer when pasting embedded color #6517
[radarhere, nulano]
- Removed EXIF prefix when saving WebP #6582
[radarhere]
- Pad IM palette to 768 bytes when saving #6579
[radarhere]
- Added DDS BC6H reading #6449
[ShadelessFox, REDxEYE, radarhere]
- Added support for opening WhiteIsZero 16-bit integer TIFF images #6642
[JayWiz, radarhere]
- Raise an error when allocating translucent color to RGB palette #6654
[jsbueno, radarhere]
- 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 - Allow default ImageDraw font to be set #6484
[radarhere, hugovk] [radarhere, hugovk]

View File

@ -25,6 +25,7 @@ exclude .coveragerc
exclude .editorconfig exclude .editorconfig
exclude .readthedocs.yml exclude .readthedocs.yml
exclude codecov.yml exclude codecov.yml
exclude renovate.json
global-exclude .git* global-exclude .git*
global-exclude *.pyc global-exclude *.pyc
global-exclude *.so global-exclude *.so

View File

@ -17,11 +17,12 @@ coverage:
.PHONY: doc .PHONY: doc
doc: doc:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs html $(MAKE) -C docs html
.PHONY: doccheck .PHONY: doccheck
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. # 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. # We don't control them. But do check, and update them to the target of their redirects.
$(MAKE) -C docs linkcheck || true $(MAKE) -C docs linkcheck || true

View File

@ -74,6 +74,9 @@ As of 2019, Pillow development is
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/Pillow/"><img
alt="Number of PyPI downloads" alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a> src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img
alt="OpenSSF Best Practices"
src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -8,6 +8,7 @@ TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
ter-x20b.pcf, from http://terminus-font.sourceforge.net/ ter-x20b.pcf, from http://terminus-font.sourceforge.net/
BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee
OpenSans.woff2, from https://fonts.googleapis.com/css?family=Open+Sans
All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to.

BIN
Tests/fonts/OpenSans.woff2 Normal file

Binary file not shown.

View File

@ -208,12 +208,11 @@ class PillowLeakTestCase:
# ru_maxrss # ru_maxrss
# This is the maximum resident set size utilized (in bytes). # This is the maximum resident set size utilized (in bytes).
return mem / 1024 # Kb return mem / 1024 # Kb
else: # linux
# linux # man 2 getrusage
# man 2 getrusage # ru_maxrss (since Linux 2.6.32)
# ru_maxrss (since Linux 2.6.32) # This is the maximum resident set size used (in kilobytes).
# This is the maximum resident set size used (in kilobytes). return mem # Kb
return mem # Kb
def _test_leak(self, core): def _test_leak(self, core):
start_mem = self._get_mem_usage() start_mem = self._get_mem_usage()
@ -285,7 +284,7 @@ def magick_command():
if imagemagick and shutil.which(imagemagick[0]): if imagemagick and shutil.which(imagemagick[0]):
return imagemagick return imagemagick
elif graphicsmagick and shutil.which(graphicsmagick[0]): if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick return graphicsmagick

BIN
Tests/images/1.eps Normal file

Binary file not shown.

BIN
Tests/images/bc6h.dds Normal file

Binary file not shown.

BIN
Tests/images/bc6h.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
Tests/images/bc6h_sf.dds Normal file

Binary file not shown.

BIN
Tests/images/bc6h_sf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Binary file not shown.

BIN
Tests/images/child_ifd.tiff Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

BIN
Tests/images/mmap_error.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
Tests/images/test_woff2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,19 +1,18 @@
import PIL from PIL import Image
import PIL.Image
def test_sanity(): def test_sanity():
# Make sure we have the binary extension # 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. # 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 (im.mode, im.size) == ("1", (100, 100))
assert len(im.tobytes()) == 1300 assert len(im.tobytes()) == 1300
# Create images in all remaining major modes. # Create images in all remaining major modes.
PIL.Image.new("L", (100, 100)) Image.new("L", (100, 100))
PIL.Image.new("P", (100, 100)) Image.new("P", (100, 100))
PIL.Image.new("RGB", (100, 100)) Image.new("RGB", (100, 100))
PIL.Image.new("I", (100, 100)) Image.new("I", (100, 100))
PIL.Image.new("F", (100, 100)) Image.new("F", (100, 100))

View File

@ -70,14 +70,14 @@ def test_libimagequant_version():
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
def test_check_modules(): @pytest.mark.parametrize("feature", features.modules)
for feature in features.modules: def test_check_modules(feature):
assert features.check_module(feature) in [True, False] assert features.check_module(feature) in [True, False]
def test_check_codecs(): @pytest.mark.parametrize("feature", features.codecs)
for feature in features.codecs: def test_check_codecs(feature):
assert features.check_codec(feature) in [True, False] assert features.check_codec(feature) in [True, False]
def test_check_warns_on_nonexistent(): def test_check_warns_on_nonexistent():

View File

@ -39,13 +39,12 @@ def test_apng_basic():
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
def test_apng_fdat(): @pytest.mark.parametrize(
with Image.open("Tests/images/apng/split_fdat.png") as im: "filename",
im.seek(im.n_frames - 1) ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
assert im.getpixel((0, 0)) == (0, 255, 0, 255) )
assert im.getpixel((64, 32)) == (0, 255, 0, 255) def test_apng_fdat(filename):
with Image.open(filename) as im:
with Image.open("Tests/images/apng/split_fdat_zero_chunk.png") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -554,18 +553,20 @@ def test_apng_save_disposal(tmp_path):
def test_apng_save_disposal_previous(tmp_path): def test_apng_save_disposal_previous(tmp_path):
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
size = (128, 64) size = (128, 64)
transparent = Image.new("RGBA", size, (0, 0, 0, 0)) blue = Image.new("RGBA", size, (0, 0, 255, 255))
red = Image.new("RGBA", size, (255, 0, 0, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
# test OP_NONE # test OP_NONE
transparent.save( blue.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, green], append_images=[red, green],
disposal=PngImagePlugin.Disposal.OP_PREVIOUS, disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
im.seek(2) im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -648,6 +649,16 @@ def test_seek_after_close():
im.seek(0) im.seek(0)
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
def test_different_modes_in_later_frames(mode, tmp_path):
test_file = str(tmp_path / "temp.png")
im = Image.new("L", (1, 1))
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
with Image.open(test_file) as reloaded:
assert reloaded.mode == mode
def test_constants_deprecation(): def test_constants_deprecation():
for enum, prefix in { for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_", PngImagePlugin.Disposal: "APNG_DISPOSE_",

View File

@ -14,6 +14,9 @@ def test_load_blp1():
with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: with Image.open("Tests/images/blp/blp1_jpeg.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
im.load()
def test_load_blp2_raw(): def test_load_blp2_raw():
with Image.open("Tests/images/blp/blp2_raw.blp") as im: with Image.open("Tests/images/blp/blp2_raw.blp") as im:

View File

@ -39,6 +39,13 @@ def test_invalid_file():
BmpImagePlugin.BmpImageFile(fp) 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(): def test_save_to_bytes():
output = io.BytesIO() output = io.BytesIO()
im = hopper() im = hopper()
@ -51,6 +58,18 @@ def test_save_to_bytes():
assert reloaded.format == "BMP" 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): def test_save_too_large(tmp_path):
outfile = str(tmp_path / "temp.bmp") outfile = str(tmp_path / "temp.bmp")
with Image.new("RGB", (1, 1)) as im: with Image.new("RGB", (1, 1)) as im:
@ -157,6 +176,11 @@ def test_rle8():
im.load() im.load()
def test_rle4():
with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"file_name,length", "file_name,length",
( (

View File

@ -16,6 +16,8 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds" TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds"
TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds"
@ -114,6 +116,20 @@ def test_dx10_bc5(image_path, expected_path):
assert_image_equal_tofile(im, expected_path.replace(".dds", ".png")) assert_image_equal_tofile(im, expected_path.replace(".dds", ".png"))
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
def test_dx10_bc6h(image_path):
"""Check DX10 BC6H/BC6HS images can be opened"""
with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
assert im.mode == "RGB"
assert im.size == (128, 128)
assert_image_equal_tofile(im, image_path.replace(".dds", ".png"))
def test_dx10_bc7(): def test_dx10_bc7():
"""Check DX10 images can be opened""" """Check DX10 images can be opened"""

View File

@ -124,14 +124,6 @@ def test_file_object(tmp_path):
image1.save(fh, "EPS") 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") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_bytesio_object(): def test_bytesio_object():
with open(FILE1, "rb") as f: with open(FILE1, "rb") as f:
@ -146,6 +138,11 @@ def test_bytesio_object():
assert_image_similar(img, image1_scale1_compare, 5) 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): def test_image_mode_not_supported(tmp_path):
im = hopper("RGBA") im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps") tmpfile = str(tmp_path / "temp.eps")
@ -198,25 +195,23 @@ def test_render_scale2():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_resize(): @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"] def test_resize(filename):
for fn in files: with Image.open(filename) as im:
with Image.open(fn) as im: new_size = (100, 100)
new_size = (100, 100) im = im.resize(new_size)
im = im.resize(new_size) assert im.size == new_size
assert im.size == new_size
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @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 # Issue #619
# Arrange # Arrange
files = [FILE1, FILE2] with Image.open(filename) as im:
for fn in files: new_size = (100, 100)
with Image.open(FILE1) as im: im.thumbnail(new_size)
new_size = (100, 100) assert max(im.size) == max(new_size)
im.thumbnail(new_size)
assert max(im.size) == max(new_size)
def test_read_binary_preview(): def test_read_binary_preview():
@ -261,20 +256,19 @@ def test_readline(tmp_path):
_test_readline_file_psfile(s, ending) _test_readline_file_psfile(s, ending)
def test_open_eps(): @pytest.mark.parametrize(
# https://github.com/python-pillow/Pillow/issues/1104 "filename",
# Arrange (
FILES = [
"Tests/images/illu10_no_preview.eps", "Tests/images/illu10_no_preview.eps",
"Tests/images/illu10_preview.eps", "Tests/images/illu10_preview.eps",
"Tests/images/illuCS6_no_preview.eps", "Tests/images/illuCS6_no_preview.eps",
"Tests/images/illuCS6_preview.eps", "Tests/images/illuCS6_preview.eps",
] ),
)
# Act / Assert def test_open_eps(filename):
for filename in FILES: # https://github.com/python-pillow/Pillow/issues/1104
with Image.open(filename) as img: with Image.open(filename) as img:
assert img.mode == "RGB" assert img.mode == "RGB"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")

View File

@ -4,7 +4,7 @@ import pytest
from PIL import FliImagePlugin, Image from PIL import FliImagePlugin, Image
from .helper import assert_image_equal_tofile, is_pypy from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# created as an export of a palette image from Gimp2.6 # created as an export of a palette image from Gimp2.6
# save as...-> hopper.fli, default options. # save as...-> hopper.fli, default options.
@ -79,6 +79,12 @@ def test_invalid_file():
FliImagePlugin.FliImageFile(invalid_file) FliImagePlugin.FliImageFile(invalid_file)
def test_palette_chunk_second():
with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
with Image.open(static_test_file) as expected:
assert_image_equal(im.convert("RGB"), expected.convert("RGB"))
def test_n_frames(): def test_n_frames():
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
assert im.n_frames == 1 assert im.n_frames == 1

View File

@ -83,18 +83,40 @@ def test_l_mode_transparency():
assert im.load()[0, 0] == 128 assert im.load()[0, 0] == 128
def test_l_mode_after_rgb():
with Image.open("Tests/images/no_palette_after_rgb.gif") as im:
im.seek(1)
assert im.mode == "RGB"
im.seek(2)
assert im.mode == "RGB"
def test_palette_not_needed_for_second_frame():
with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
im.seek(1)
assert_image_similar(im, hopper("L").convert("RGB"), 8)
def test_strategy(): def test_strategy():
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
expected_zero = im.convert("RGB") expected_rgb_always_rgba = im.convert("RGBA")
im.seek(1) im.seek(1)
expected_one = im.convert("RGB") expected_different = im.convert("RGB")
try: try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert_image_equal(im, expected_zero) assert_image_equal(im, expected_rgb_always)
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)
GifImagePlugin.LOADING_STRATEGY = ( GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
@ -105,7 +127,7 @@ def test_strategy():
im.seek(1) im.seek(1)
assert im.mode == "P" assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_one) assert_image_equal(im.convert("RGB"), expected_different)
# Change to RGB mode when a frame has an individual palette # Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im: with Image.open("Tests/images/iss634.gif") as im:
@ -793,24 +815,24 @@ def test_identical_frames(tmp_path):
assert reread.info["duration"] == 4500 assert reread.info["duration"] == 4500
def test_identical_frames_to_single_frame(tmp_path): @pytest.mark.parametrize(
for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)
out = str(tmp_path / "temp.gif") )
im_list = [ def test_identical_frames_to_single_frame(duration, tmp_path):
Image.new("L", (100, 100), "#000"), out = str(tmp_path / "temp.gif")
Image.new("L", (100, 100), "#000"), im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
] Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
]
im_list[0].save( im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
out, save_all=True, append_images=im_list[1:], duration=duration with Image.open(out) as reread:
) # Assert that all frames were combined
with Image.open(out) as reread: assert reread.n_frames == 1
# Assert that all frames were combined
assert reread.n_frames == 1
# Assert that the new duration is the total of the identical frames # Assert that the new duration is the total of the identical frames
assert reread.info["duration"] == 8500 assert reread.info["duration"] == 8500
def test_number_of_loops(tmp_path): def test_number_of_loops(tmp_path):
@ -1087,6 +1109,19 @@ def test_palette_save_P(tmp_path):
assert_image_equal(reloaded, im) 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): def test_palette_save_all_P(tmp_path):
frames = [] frames = []
colors = ((255, 0, 0), (0, 255, 0)) colors = ((255, 0, 0), (0, 255, 0))

View File

@ -86,6 +86,18 @@ def test_roundtrip(mode, tmp_path):
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 1, 2]
im.putpalette(colors)
out = str(tmp_path / "temp.im")
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors + [0] * 765
def test_save_unsupported_mode(tmp_path): def test_save_unsupported_mode(tmp_path):
out = str(tmp_path / "temp.im") out = str(tmp_path / "temp.im")
im = hopper("HSV") im = hopper("HSV")

19
Tests/test_file_imt.py Normal file
View File

@ -0,0 +1,19 @@
import io
import pytest
from PIL import Image, ImtImagePlugin
from .helper import assert_image_equal_tofile
def test_sanity():
with Image.open("Tests/images/bw_gradient.imt") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n"))
def test_invalid_file(data):
with io.BytesIO(data) as fp:
with pytest.raises(SyntaxError):
ImtImagePlugin.ImtImageFile(fp)

View File

@ -30,7 +30,7 @@ from .helper import (
) )
try: try:
import defusedxml.ElementTree as ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:
ElementTree = None ElementTree = None
@ -150,27 +150,30 @@ class TestFileJpeg:
assert not im1.info.get("icc_profile") assert not im1.info.get("icc_profile")
assert im2.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 # Make sure that the "extra" support handles large blocks
def test(n): # The ICC APP marker can store 65519 bytes per marker, so
# The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of
# using a 4-byte test code should allow us to detect out of # order issues.
# order issues. icc_profile = (b"Test" * int(n / 4 + 1))[:n]
icc_profile = (b"Test" * int(n / 4 + 1))[:n] assert len(icc_profile) == n # sanity
assert len(icc_profile) == n # sanity im1 = self.roundtrip(hopper(), icc_profile=icc_profile)
im1 = self.roundtrip(hopper(), icc_profile=icc_profile) assert im1.info.get("icc_profile") == (icc_profile or None)
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
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -649,19 +652,19 @@ class TestFileJpeg:
# Assert # Assert
assert im.format == "JPEG" 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() out = BytesIO()
for mode in ["1", "L", "RGB", "RGBX", "CMYK", "YCbCr"]: img = Image.new(mode, (20, 20))
img = Image.new(mode, (20, 20)) img.save(out, "JPEG")
img.save(out, "JPEG")
def test_save_wrong_modes(self): @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P"))
def test_save_wrong_modes(self, mode):
# ref https://github.com/python-pillow/Pillow/issues/2005 # ref https://github.com/python-pillow/Pillow/issues/2005
out = BytesIO() out = BytesIO()
for mode in ["LA", "La", "RGBA", "RGBa", "P"]: img = Image.new(mode, (20, 20))
img = Image.new(mode, (20, 20)) with pytest.raises(OSError):
with pytest.raises(OSError): img.save(out, "JPEG")
img.save(out, "JPEG")
def test_save_tiff_with_dpi(self, tmp_path): def test_save_tiff_with_dpi(self, tmp_path):
# Arrange # Arrange

View File

@ -126,14 +126,14 @@ def test_prog_res_rt():
assert_image_equal(im, test_card) assert_image_equal(im, test_card)
def test_default_num_resolutions(): @pytest.mark.parametrize("num_resolutions", range(2, 6))
for num_resolutions in range(2, 6): def test_default_num_resolutions(num_resolutions):
d = 1 << (num_resolutions - 1) d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1)) im = test_card.resize((d - 1, d - 1))
with pytest.raises(OSError): with pytest.raises(OSError):
roundtrip(im, num_resolutions=num_resolutions) roundtrip(im, num_resolutions=num_resolutions)
reloaded = roundtrip(im) reloaded = roundtrip(im)
assert_image_equal(im, reloaded) assert_image_equal(im, reloaded)
def test_reduce(): def test_reduce():
@ -266,14 +266,11 @@ def test_rgba():
assert jp2.mode == "RGBA" assert jp2.mode == "RGBA"
def test_16bit_monochrome_has_correct_mode(): @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
with Image.open("Tests/images/16bit.cropped.j2k") as j2k: def test_16bit_monochrome_has_correct_mode(ext):
j2k.load() with Image.open("Tests/images/16bit.cropped" + ext) as im:
assert j2k.mode == "I;16" im.load()
assert im.mode == "I;16"
with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
jp2.load()
assert jp2.mode == "I;16"
def test_16bit_monochrome_jp2_like_tiff(): def test_16bit_monochrome_jp2_like_tiff():

View File

@ -3,6 +3,7 @@ import io
import itertools import itertools
import os import os
import re import re
import sys
from collections import namedtuple from collections import namedtuple
import pytest import pytest
@ -509,20 +510,13 @@ class TestFileLibTiff(LibTiffTestCase):
# colormap/palette tag # colormap/palette tag
assert len(reloaded.tag_v2[320]) == 768 assert len(reloaded.tag_v2[320]) == 768
def xtest_bw_compression_w_rgb(self, tmp_path): @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
"""This test passes, but when running all tests causes a failure due def test_bw_compression_w_rgb(self, compression, tmp_path):
to output on stderr from the error thrown by libtiff. We need to
capture that but not now"""
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(out, compression="tiff_ccitt") im.save(out, compression=compression)
with pytest.raises(OSError):
im.save(out, compression="group3")
with pytest.raises(OSError):
im.save(out, compression="group4")
def test_fp_leak(self): def test_fp_leak(self):
im = Image.open("Tests/images/hopper_g4_500.tif") im = Image.open("Tests/images/hopper_g4_500.tif")
@ -832,6 +826,44 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.mode == "F" assert reloaded.mode == "F"
assert reloaded.getexif()[SAMPLEFORMAT] == 3 assert reloaded.getexif()[SAMPLEFORMAT] == 3
def test_lzma(self, capfd):
try:
with Image.open("Tests/images/hopper_lzma.tif") as im:
assert im.mode == "RGB"
assert im.size == (128, 128)
assert im.format == "TIFF"
im2 = hopper()
assert_image_similar(im, im2, 5)
except OSError:
captured = capfd.readouterr()
if "LZMA compression support is not configured" in captured.err:
pytest.skip("LZMA compression support is not configured")
sys.stdout.write(captured.out)
sys.stderr.write(captured.err)
raise
def test_webp(self, capfd):
try:
with Image.open("Tests/images/hopper_webp.tif") as im:
assert im.mode == "RGB"
assert im.size == (128, 128)
assert im.format == "TIFF"
assert_image_similar_tofile(im, "Tests/images/hopper_webp.png", 1)
except OSError:
captured = capfd.readouterr()
if "WEBP compression support is not configured" in captured.err:
pytest.skip("WEBP compression support is not configured")
if (
"Compression scheme 50001 strip decoding is not implemented"
in captured.err
):
pytest.skip(
"Compression scheme 50001 strip decoding is not implemented"
)
sys.stdout.write(captured.out)
sys.stderr.write(captured.err)
raise
def test_lzw(self): def test_lzw(self):
with Image.open("Tests/images/hopper_lzw.tif") as im: with Image.open("Tests/images/hopper_lzw.tif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
@ -941,7 +973,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, exif=tags, compression=compression) im.save(out, exif=tags, compression=compression)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
for tag in tags.keys(): for tag in tags:
assert tag not in reloaded.getexif() assert tag not in reloaded.getexif()
def test_old_style_jpeg(self): def test_old_style_jpeg(self):

View File

@ -63,19 +63,7 @@ def test_p_mode(tmp_path):
roundtrip(tmp_path, mode) roundtrip(tmp_path, mode)
def test_l_oserror(tmp_path): @pytest.mark.parametrize("mode", ("L", "RGB"))
# Arrange def test_oserror(tmp_path, mode):
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
with pytest.raises(OSError): with pytest.raises(OSError):
helper_save_as_palm(tmp_path, mode) helper_save_as_palm(tmp_path, mode)

View File

@ -39,14 +39,14 @@ def test_invalid_file():
PcxImagePlugin.PcxImageFile(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. # See issue #523, odd sized images should have a stride that's even.
# Not that ImageMagick or GIMP write PCX that way. # Not that ImageMagick or GIMP write PCX that way.
# We were not handling properly. # We were not handling properly.
for mode in ("1", "L", "P", "RGB"): # larger, odd sized images are better here to ensure that
# larger, odd sized images are better here to ensure that # we handle interrupted scan lines properly.
# we handle interrupted scan lines properly. _roundtrip(tmp_path, hopper(mode).resize((511, 511)))
_roundtrip(tmp_path, hopper(mode).resize((511, 511)))
def test_odd_read(): def test_odd_read():

View File

@ -6,7 +6,7 @@ import time
import pytest import pytest
from PIL import Image, PdfParser from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version from .helper import hopper, mark_if_feature_version
@ -37,6 +37,11 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
return outfile 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") @pytest.mark.valgrind_known_error(reason="Temporary skip")
def test_monochrome(tmp_path): def test_monochrome(tmp_path):
# Arrange # Arrange
@ -44,39 +49,7 @@ def test_monochrome(tmp_path):
# Act / Assert # Act / Assert
outfile = helper_save_as_pdf(tmp_path, mode) outfile = helper_save_as_pdf(tmp_path, mode)
assert os.path.getsize(outfile) < 5000 assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 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)
def test_unsupported_mode(tmp_path): def test_unsupported_mode(tmp_path):

View File

@ -20,7 +20,7 @@ from .helper import (
) )
try: try:
import defusedxml.ElementTree as ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:
ElementTree = None ElementTree = None

View File

@ -240,8 +240,8 @@ def test_header_token_too_long(tmp_path):
def test_truncated_file(tmp_path): def test_truncated_file(tmp_path):
# Test EOF in header # Test EOF in header
path = str(tmp_path / "temp.pgm") path = str(tmp_path / "temp.pgm")
with open(path, "w") as f: with open(path, "wb") as f:
f.write("P6") f.write(b"P6")
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
with Image.open(path): with Image.open(path):
@ -256,11 +256,11 @@ def test_truncated_file(tmp_path):
im.load() im.load()
@pytest.mark.parametrize("maxval", (0, 65536)) @pytest.mark.parametrize("maxval", (b"0", b"65536"))
def test_invalid_maxval(maxval, tmp_path): def test_invalid_maxval(maxval, tmp_path):
path = str(tmp_path / "temp.ppm") path = str(tmp_path / "temp.ppm")
with open(path, "w") as f: with open(path, "wb") as f:
f.write("P6\n3 1 " + str(maxval)) f.write(b"P6\n3 1 " + maxval)
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
with Image.open(path): with Image.open(path):
@ -283,13 +283,13 @@ def test_neg_ppm():
def test_mimetypes(tmp_path): def test_mimetypes(tmp_path):
path = str(tmp_path / "temp.pgm") path = str(tmp_path / "temp.pgm")
with open(path, "w") as f: with open(path, "wb") as f:
f.write("P4\n128 128\n255") f.write(b"P4\n128 128\n255")
with Image.open(path) as im: with Image.open(path) as im:
assert im.get_format_mimetype() == "image/x-portable-bitmap" assert im.get_format_mimetype() == "image/x-portable-bitmap"
with open(path, "w") as f: with open(path, "wb") as f:
f.write("PyCMYK\n128 128\n255") f.write(b"PyCMYK\n128 128\n255")
with Image.open(path) as im: with Image.open(path) as im:
assert im.get_format_mimetype() == "image/x-portable-anymap" assert im.get_format_mimetype() == "image/x-portable-anymap"

View File

@ -120,6 +120,18 @@ def test_save(tmp_path):
assert test_im.size == (100, 100) 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): def test_save_wrong_mode(tmp_path):
im = hopper("PA") im = hopper("PA")
out = str(tmp_path / "temp.tga") out = str(tmp_path / "temp.tga")

View File

@ -18,7 +18,7 @@ from .helper import (
) )
try: try:
import defusedxml.ElementTree as ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:
ElementTree = None ElementTree = None
@ -84,6 +84,24 @@ class TestFileTiff:
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
im.load() 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): def test_mac_tiff(self):
# Read RGBa images from macOS [@PIL136] # Read RGBa images from macOS [@PIL136]
@ -293,14 +311,17 @@ class TestFileTiff:
with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"):
pass pass
def test_n_frames(self): @pytest.mark.parametrize(
for path, n_frames in [ "path, n_frames",
["Tests/images/multipage-lastframe.tif", 1], (
["Tests/images/multipage.tiff", 3], ("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) 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): def test_eoferror(self):
with Image.open("Tests/images/multipage-lastframe.tif") as im: with Image.open("Tests/images/multipage-lastframe.tif") as im:
@ -416,12 +437,12 @@ class TestFileTiff:
len_after = len(dict(im.ifd)) len_after = len(dict(im.ifd))
assert len_before == len_after + 1 assert len_before == len_after + 1
def test_load_byte(self): @pytest.mark.parametrize("legacy_api", (False, True))
for legacy_api in [False, True]: def test_load_byte(self, legacy_api):
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abc" data = b"abc"
ret = ifd.load_byte(data, legacy_api) ret = ifd.load_byte(data, legacy_api)
assert ret == b"abc" assert ret == b"abc"
def test_load_string(self): def test_load_string(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
@ -667,18 +688,15 @@ class TestFileTiff:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile) assert_image_equal_tofile(reloaded, infile)
def test_palette(self, tmp_path): @pytest.mark.parametrize("mode", ("P", "PA"))
def roundtrip(mode): def test_palette(self, mode, tmp_path):
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im = hopper(mode) im = hopper(mode)
im.save(outfile) im.save(outfile)
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
for mode in ["P", "PA"]:
roundtrip(mode)
def test_tiff_save_all(self): def test_tiff_save_all(self):
mp = BytesIO() mp = BytesIO()

View File

@ -185,6 +185,22 @@ def test_iptc(tmp_path):
im.save(out) 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): def test_undefined_zero(tmp_path):
# Check that the tag has not been changed since this test was created # Check that the tag has not been changed since this test was created
tag = TiffTags.TAGS_V2[45059] tag = TiffTags.TAGS_V2[45059]

View File

@ -55,9 +55,7 @@ def test_write_exif_metadata():
test_buffer.seek(0) test_buffer.seek(0)
with Image.open(test_buffer) as webp_image: with Image.open(test_buffer) as webp_image:
webp_exif = webp_image.info.get("exif", None) webp_exif = webp_image.info.get("exif", None)
assert webp_exif assert webp_exif == expected_exif[6:], "WebP EXIF didn't match"
if webp_exif:
assert webp_exif == expected_exif, "WebP EXIF didn't match"
def test_read_icc_profile(): def test_read_icc_profile():

View File

@ -1,5 +1,7 @@
import os import os
import pytest
from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
from .helper import ( from .helper import (
@ -59,23 +61,13 @@ def save_font(request, tmp_path, encoding):
return tempname 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) save_font(request, tmp_path, encoding)
def test_sanity_iso8859_1(request, tmp_path): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
_test_sanity(request, tmp_path, "iso8859-1") def test_draw(request, tmp_path, encoding):
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):
tempname = save_font(request, tmp_path, encoding) tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)
im = Image.new("L", (150, 30), "white") im = Image.new("L", (150, 30), "white")
@ -85,19 +77,8 @@ def _test_draw(request, tmp_path, encoding):
assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) assert_image_similar_tofile(im, charsets[encoding]["image1"], 0)
def test_draw_iso8859_1(request, tmp_path): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
_test_draw(request, tmp_path, "iso8859-1") def test_textsize(request, tmp_path, encoding):
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):
tempname = save_font(request, tmp_path, encoding) tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname) font = ImageFont.load(tempname)
for i in range(255): for i in range(255):
@ -112,15 +93,3 @@ def _test_textsize(request, tmp_path, encoding):
msg = message[: i + 1] msg = message[: i + 1]
assert font.getlength(msg) == len(msg) * 10 assert font.getlength(msg) == len(msg) * 10
assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) assert font.getbbox(msg) == (0, 0, 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")

View File

@ -129,8 +129,6 @@ class TestImage:
im.size = (3, 4) im.size = (3, 4)
def test_invalid_image(self): def test_invalid_image(self):
import io
im = io.BytesIO(b"") im = io.BytesIO(b"")
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
with Image.open(im): with Image.open(im):
@ -620,6 +618,7 @@ class TestImage:
im_remapped = im.remap_palette([1, 0]) im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1 assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6
# Test unused transparency # Test unused transparency
im.info["transparency"] = 2 im.info["transparency"] = 2
@ -698,15 +697,15 @@ class TestImage:
def test_empty_exif(self): def test_empty_exif(self):
with Image.open("Tests/images/exif.png") as im: with Image.open("Tests/images/exif.png") as im:
exif = im.getexif() exif = im.getexif()
assert dict(exif) != {} assert dict(exif)
# Test that exif data is cleared after another load # Test that exif data is cleared after another load
exif.load(None) exif.load(None)
assert dict(exif) == {} assert not dict(exif)
# Test loading just the EXIF header # Test loading just the EXIF header
exif.load(b"Exif\x00\x00") exif.load(b"Exif\x00\x00")
assert dict(exif) == {} assert not dict(exif)
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"

View File

@ -131,8 +131,7 @@ class TestImageGetPixel(AccessTest):
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
if bands == 1: if bands == 1:
return 1 return 1
else: return tuple(range(1, bands + 1))
return tuple(range(1, bands + 1))
def check(self, mode, c=None): def check(self, mode, c=None):
if not c: if not c:
@ -215,11 +214,14 @@ class TestImageGetPixel(AccessTest):
self.check(mode, 2**15 + 1) self.check(mode, 2**15 + 1)
self.check(mode, 2**16 - 1) self.check(mode, 2**16 - 1)
@pytest.mark.parametrize("mode", ("P", "PA"))
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
def test_p_putpixel_rgb_rgba(self, color): def test_p_putpixel_rgb_rgba(self, mode, color):
im = Image.new("P", (1, 1), 0) im = Image.new(mode, (1, 1))
im.putpixel((0, 0), color) im.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)
@pytest.mark.skipif(cffi is None, reason="No CFFI") @pytest.mark.skipif(cffi is None, reason="No CFFI")
@ -340,12 +342,16 @@ class TestCffi(AccessTest):
# pixels can contain garbage if image is released # pixels can contain garbage if image is released
assert px[i, 0] == 0 assert px[i, 0] == 0
def test_p_putpixel_rgb_rgba(self): @pytest.mark.parametrize("mode", ("P", "PA"))
for color in [(255, 0, 0), (255, 0, 0, 255)]: def test_p_putpixel_rgb_rgba(self, mode):
im = Image.new("P", (1, 1), 0) for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
access = PyAccess.new(im, False) access = PyAccess.new(im, False)
access.putpixel((0, 0), color) access.putpixel((0, 0), color)
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
if len(color) == 3:
color += (255,)
assert im.convert("RGBA").getpixel((0, 0)) == color
class TestImagePutPixelError(AccessTest): class TestImagePutPixelError(AccessTest):
@ -408,7 +414,7 @@ class TestEmbeddable:
def test_embeddable(self): def test_embeddable(self):
import ctypes import ctypes
with open("embed_pil.c", "w") as fh: with open("embed_pil.c", "w", encoding="utf-8") as fh:
fh.write( fh.write(
""" """
#include "Python.h" #include "Python.h"

View File

@ -35,10 +35,13 @@ def test_toarray():
test_with_dtype(numpy.float64) test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8) test_with_dtype(numpy.uint8)
if parse_version(numpy.__version__) >= parse_version("1.23"): with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: if parse_version(numpy.__version__) >= parse_version("1.23"):
with pytest.raises(OSError): with pytest.raises(OSError):
numpy.array(im_truncated) numpy.array(im_truncated)
else:
with pytest.warns(UserWarning):
numpy.array(im_truncated)
def test_fromarray(): def test_fromarray():

View File

@ -38,6 +38,12 @@ def test_sanity():
convert(im, output_mode) convert(im, output_mode)
def test_unsupported_conversion():
im = hopper()
with pytest.raises(ValueError):
im.convert("INVALID")
def test_default(): def test_default():
im = hopper("P") im = hopper("P")
@ -236,6 +242,23 @@ def test_p2pa_alpha():
assert im_a.getpixel((x, y)) == 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()
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
def test_rgb_lab(mode):
im = Image.new(mode, (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
def test_matrix_illegal_conversion(): def test_matrix_illegal_conversion():
# Arrange # Arrange
im = hopper("CMYK") im = hopper("CMYK")

View File

@ -5,90 +5,109 @@ from PIL import Image, ImageFilter
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
def test_sanity(): @pytest.mark.parametrize(
def apply_filter(filter_to_apply): "filter_to_apply",
for mode in ["L", "RGB", "CMYK"]: (
im = hopper(mode) ImageFilter.BLUR,
out = im.filter(filter_to_apply) ImageFilter.CONTOUR,
assert out.mode == im.mode ImageFilter.DETAIL,
assert out.size == im.size 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): with pytest.raises(TypeError):
apply_filter("hello") im = hopper(mode)
im.filter("hello")
def test_crash(): # crashes on small images
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
# crashes on small images def test_crash(size):
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", size)
im.filter(ImageFilter.SMOOTH)
im = Image.new("RGB", (2, 2))
im.filter(ImageFilter.SMOOTH)
im = Image.new("RGB", (3, 3))
im.filter(ImageFilter.SMOOTH) im.filter(ImageFilter.SMOOTH)
def test_modefilter(): @pytest.mark.parametrize(
def modefilter(mode): "mode, expected",
im = Image.new(mode, (3, 3), None) (
im.putdata(list(range(9))) ("1", (4, 0)),
# image is: ("L", (4, 0)),
# 0 1 2 ("P", (4, 0)),
# 3 4 5 ("RGB", ((4, 0, 0), (0, 0, 0))),
# 6 7 8 ),
mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) )
im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 def test_modefilter(mode, expected):
mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) im = Image.new(mode, (3, 3), None)
return mod, mod2 im.putdata(list(range(9)))
# image is:
assert modefilter("1") == (4, 0) # 0 1 2
assert modefilter("L") == (4, 0) # 3 4 5
assert modefilter("P") == (4, 0) # 6 7 8
assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0)) 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(): @pytest.mark.parametrize(
def rankfilter(mode): "mode, expected",
im = Image.new(mode, (3, 3), None) (
im.putdata(list(range(9))) ("1", (0, 4, 8)),
# image is: ("L", (0, 4, 8)),
# 0 1 2 ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))),
# 3 4 5 ("I", (0, 4, 8)),
# 6 7 8 ("F", (0.0, 4.0, 8.0)),
minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) ),
med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) )
maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) def test_rankfilter(mode, expected):
return minimum, med, maximum 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): with pytest.raises(ValueError):
rankfilter("P") im = Image.new("P", (3, 3), None)
assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0)) im.putdata(list(range(9)))
assert rankfilter("I") == (0, 4, 8) # image is:
assert rankfilter("F") == (0.0, 4.0, 8.0) # 0 1 2
# 3 4 5
# 6 7 8
im.filter(filter).getpixel((1, 1))
def test_rankfilter_properties(): def test_rankfilter_properties():
@ -110,7 +129,8 @@ def test_kernel_not_enough_coefficients():
ImageFilter.Kernel((3, 3), (0, 0)) 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.bmp") as source:
with Image.open("Tests/images/hopper_emboss.bmp") as reference: with Image.open("Tests/images/hopper_emboss.bmp") as reference:
kernel = ImageFilter.Kernel( kernel = ImageFilter.Kernel(
@ -125,14 +145,14 @@ def test_consistency_3x3():
source = source.split() * 2 source = source.split() * 2
reference = reference.split() * 2 reference = reference.split() * 2
for mode in ["L", "LA", "RGB", "CMYK"]: assert_image_equal(
assert_image_equal( Image.merge(mode, source[: len(mode)]).filter(kernel),
Image.merge(mode, source[: len(mode)]).filter(kernel), Image.merge(mode, reference[: len(mode)]),
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.bmp") as source:
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
kernel = ImageFilter.Kernel( kernel = ImageFilter.Kernel(
@ -149,8 +169,7 @@ def test_consistency_5x5():
source = source.split() * 2 source = source.split() * 2
reference = reference.split() * 2 reference = reference.split() * 2
for mode in ["L", "LA", "RGB", "CMYK"]: assert_image_equal(
assert_image_equal( Image.merge(mode, source[: len(mode)]).filter(kernel),
Image.merge(mode, source[: len(mode)]).filter(kernel), Image.merge(mode, reference[: len(mode)]),
Image.merge(mode, reference[: len(mode)]), )
)

View File

@ -38,58 +38,64 @@ gradients_image = Image.open("Tests/images/radial_gradients.png")
gradients_image.load() 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)) im = Image.new("L", (10, 10))
assert expected == im.reduce(size).size
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))
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)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size im.reduce(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))
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)) im = Image.new("P", (10, 10))
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.reduce(3) 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): def get_image(mode):
mode_info = ImageMode.getmode(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") im = get_image("L")
for factor in remarkable_factors: compare_reduce_with_reference(im, factor)
compare_reduce_with_reference(im, factor) compare_reduce_with_box(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") 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. # With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255)) im.putalpha(Image.new("L", im.size, 255))
for factor in remarkable_factors: compare_reduce_with_reference(im, factor)
compare_reduce_with_reference(im, factor) compare_reduce_with_box(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") im = get_image("La")
for factor in remarkable_factors: compare_reduce_with_reference(im, factor)
compare_reduce_with_reference(im, factor) compare_reduce_with_box(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") im = get_image("RGB")
for factor in remarkable_factors: compare_reduce_with_reference(im, factor)
compare_reduce_with_reference(im, factor) compare_reduce_with_box(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") 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. # With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255)) im.putalpha(Image.new("L", im.size, 255))
for factor in remarkable_factors: compare_reduce_with_reference(im, factor)
compare_reduce_with_reference(im, factor) compare_reduce_with_box(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") im = get_image("RGBa")
for factor in remarkable_factors: compare_reduce_with_reference(im, factor)
compare_reduce_with_reference(im, factor) compare_reduce_with_box(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") im = get_image("I")
for factor in remarkable_factors: compare_reduce_with_reference(im, factor)
compare_reduce_with_reference(im, factor) compare_reduce_with_box(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") im = get_image("F")
for factor in remarkable_factors: compare_reduce_with_reference(im, factor, 0, 0)
compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor)
compare_reduce_with_box(im, factor)
@skip_unless_feature("jpg_2000") @skip_unless_feature("jpg_2000")

View File

@ -554,44 +554,48 @@ class TestCoreResampleBox:
# check that the difference at least that much # check that the difference at least that much
assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}") 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 # Can skip resize for one dimension
im = hopper() im = hopper()
for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: for size, box in [
for size, box in [ ((40, 50), (0, 0, 40, 90)),
((40, 50), (0, 0, 40, 90)), ((40, 50), (0, 20, 40, 90)),
((40, 50), (0, 20, 40, 90)), ((40, 50), (10, 0, 50, 90)),
((40, 50), (10, 0, 50, 90)), ((40, 50), (10, 20, 50, 90)),
((40, 50), (10, 20, 50, 90)), ]:
]: res = im.resize(size, flt, box)
res = im.resize(size, flt, box) assert res.size == size
assert res.size == size # Borders should be slightly different
# Borders should be slightly different assert_image_similar(
assert_image_similar( res,
res, im.crop(box).resize(size, flt),
im.crop(box).resize(size, flt), 0.4,
0.4, f">>> {size} {box} {flt}",
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 # Can skip resize for one dimension
im = hopper() im = hopper()
for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: for size, box in [
for size, box in [ ((40, 50), (0, 0, 90, 50)),
((40, 50), (0, 0, 90, 50)), ((40, 50), (20, 0, 90, 50)),
((40, 50), (20, 0, 90, 50)), ((40, 50), (0, 10, 90, 60)),
((40, 50), (0, 10, 90, 60)), ((40, 50), (20, 10, 90, 60)),
((40, 50), (20, 10, 90, 60)), ]:
]: res = im.resize(size, flt, box)
res = im.resize(size, flt, box) assert res.size == size
assert res.size == size # Borders should be slightly different
# Borders should be slightly different assert_image_similar(
assert_image_similar( res,
res, im.crop(box).resize(size, flt),
im.crop(box).resize(size, flt), 0.4,
0.4, f">>> {size} {box} {flt}",
f">>> {size} {box} {flt}", )
)

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image, features from PIL import Image, features
from .helper import assert_image_equal, hopper 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)] assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)]
def test_split_merge(): @pytest.mark.parametrize(
def split_merge(mode): "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr")
return Image.merge(mode, hopper(mode).split()) )
def test_split_merge(mode):
assert_image_equal(hopper("1"), split_merge("1")) expected = Image.merge(mode, hopper(mode).split())
assert_image_equal(hopper("L"), split_merge("L")) assert_image_equal(hopper(mode), expected)
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"))
def test_split_open(tmp_path): def test_split_open(tmp_path):

View File

@ -97,6 +97,28 @@ def test_load_first():
im.thumbnail((64, 64)) im.thumbnail((64, 64))
assert im.size == (64, 10) 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 # valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing") @pytest.mark.valgrind_known_error(reason="Known Failing")

View File

@ -75,23 +75,25 @@ class TestImageTransform:
assert_image_equal(transformed, scaled) assert_image_equal(transformed, scaled)
def test_fill(self): @pytest.mark.parametrize(
for mode, pixel in [ "mode, expected_pixel",
["RGB", (255, 0, 0)], (
["RGBA", (255, 0, 0, 255)], ("RGB", (255, 0, 0)),
["LA", (76, 0)], ("RGBA", (255, 0, 0, 255)),
]: ("LA", (76, 0)),
im = hopper(mode) ),
(w, h) = im.size )
transformed = im.transform( def test_fill(self, mode, expected_pixel):
im.size, im = hopper(mode)
Image.Transform.EXTENT, (w, h) = im.size
(0, 0, w * 2, h * 2), transformed = im.transform(
Image.Resampling.BILINEAR, im.size,
fillcolor="red", Image.Transform.EXTENT,
) (0, 0, w * 2, h * 2),
Image.Resampling.BILINEAR,
assert transformed.getpixel((w - 1, h - 1)) == pixel fillcolor="red",
)
assert transformed.getpixel((w - 1, h - 1)) == expected_pixel
def test_mesh(self): def test_mesh(self):
# this should be a checkerboard of halfsized hoppers in ul, lr # this should be a checkerboard of halfsized hoppers in ul, lr
@ -222,14 +224,12 @@ class TestImageTransform:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.transform((100, 100), None) 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: with hopper() as im:
(w, h) = im.size (w, h) = im.size
for resample in (Image.Resampling.BOX, "unknown"): with pytest.raises(ValueError):
with pytest.raises(ValueError): im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample)
im.transform(
(100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample
)
class TestImageTransformAffine: class TestImageTransformAffine:
@ -239,7 +239,16 @@ class TestImageTransformAffine:
im = hopper("RGB") im = hopper("RGB")
return im.crop((10, 20, im.width - 10, im.height - 20)) 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() im = self._test_image()
angle = -math.radians(deg) angle = -math.radians(deg)
@ -271,77 +280,65 @@ class TestImageTransformAffine:
) )
assert_image_equal(transposed, transformed) assert_image_equal(transposed, transformed)
def test_rotate_0_deg(self): @pytest.mark.parametrize(
self._test_rotate(0, None) "scale, epsilon_scale",
(
def test_rotate_90_deg(self): (1.1, 6.9),
self._test_rotate(90, Image.Transpose.ROTATE_90) (1.5, 5.5),
(2.0, 5.5),
def test_rotate_180_deg(self): (2.3, 3.7),
self._test_rotate(180, Image.Transpose.ROTATE_180) (2.5, 3.7),
),
def test_rotate_270_deg(self): )
self._test_rotate(270, Image.Transpose.ROTATE_270) @pytest.mark.parametrize(
"resample,epsilon",
def _test_resize(self, scale, epsilonscale): (
(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() im = self._test_image()
size_up = int(round(im.width * scale)), int(round(im.height * scale)) 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_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0]
matrix_down = [scale, 0, 0, 0, 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.NEAREST, 0),
(Image.Resampling.BILINEAR, 2), (Image.Resampling.BILINEAR, 1.5),
(Image.Resampling.BICUBIC, 1), (Image.Resampling.BICUBIC, 1),
]: ),
transformed = im.transform(size_up, self.transform, matrix_up, resample) )
transformed = transformed.transform( def test_translate(self, x, y, epsilon_scale, resample, epsilon):
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):
im = self._test_image() im = self._test_image()
size_up = int(round(im.width + x)), int(round(im.height + y)) size_up = int(round(im.width + x)), int(round(im.height + y))
matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] matrix_up = [1, 0, -x, 0, 1, -y, 0, 0]
matrix_down = [1, 0, x, 0, 1, y, 0, 0] matrix_down = [1, 0, x, 0, 1, y, 0, 0]
for resample, epsilon in [ transformed = im.transform(size_up, self.transform, matrix_up, resample)
(Image.Resampling.NEAREST, 0), transformed = transformed.transform(
(Image.Resampling.BILINEAR, 1.5), im.size, self.transform, matrix_down, resample
(Image.Resampling.BICUBIC, 1), )
]: assert_image_similar(transformed, im, epsilon * epsilon_scale)
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)
class TestImageTransformPerspective(TestImageTransformAffine): class TestImageTransformPerspective(TestImageTransformAffine):

View File

@ -64,7 +64,9 @@ def test_mode_mismatch():
ImageDraw.ImageDraw(im, mode="L") 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) 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) 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(): def test_arc_end_le_start():
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -192,29 +184,21 @@ def test_bitmap():
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") 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 # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
expected = f"Tests/images/imagedraw_chord_{mode}.png" expected = f"Tests/images/imagedraw_chord_{mode}.png"
# Act # Act
draw.chord(bbox, start, end, fill="red", outline="yellow") draw.chord(bbox, 0, 180, fill="red", outline="yellow")
# Assert # Assert
assert_image_similar_tofile(im, expected, 1) 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(): def test_chord_width():
# Arrange # Arrange
im = Image.new("RGB", (W, H)) 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") 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 # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -276,16 +262,6 @@ def helper_ellipse(mode, bbox):
assert_image_similar_tofile(im, expected, 1) 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(): def test_ellipse_translucent():
# Arrange # Arrange
im = Image.new("RGB", (W, H)) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -417,14 +394,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") 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(): def test_shape1():
# Arrange # Arrange
im = Image.new("RGB", (100, 100), "white") im = Image.new("RGB", (100, 100), "white")
@ -484,7 +453,9 @@ def test_transform():
assert_image_equal(im, expected) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) 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) 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(): def test_pieslice_width():
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -585,7 +546,8 @@ def test_pieslice_no_spikes():
assert_image_equal(im, im_pre_erase) assert_image_equal(im, im_pre_erase)
def helper_point(points): @pytest.mark.parametrize("points", (POINTS1, POINTS2))
def test_point(points):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -597,15 +559,8 @@ def helper_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
def test_point1(): @pytest.mark.parametrize("points", (POINTS1, POINTS2))
helper_point(POINTS1) def test_polygon(points):
def test_point2():
helper_point(POINTS2)
def helper_polygon(points):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -617,14 +572,6 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
def test_polygon1():
helper_polygon(POINTS1)
def test_polygon2():
helper_polygon(POINTS2)
@pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("mode", ("RGB", "L"))
def test_polygon_kite(mode): def test_polygon_kite(mode):
# Test drawing lines of different gradients (dx>dy, dy>dx) and # Test drawing lines of different gradients (dx>dy, dy>dx) and
@ -682,7 +629,8 @@ def test_polygon_translucent():
assert_image_equal_tofile(im, expected) assert_image_equal_tofile(im, expected)
def helper_rectangle(bbox): @pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
def test_rectangle(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -694,14 +642,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") 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(): def test_big_rectangle():
# Test drawing a rectangle bigger than the image # Test drawing a rectangle bigger than the image
# Arrange # Arrange
@ -1503,7 +1443,7 @@ def test_discontiguous_corners_polygon():
assert_image_similar_tofile(img, expected, 1) assert_image_similar_tofile(img, expected, 1)
def test_polygon(): def test_polygon2():
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")

View File

@ -52,27 +52,19 @@ def test_sanity():
draw.line(list(range(10)), pen) draw.line(list(range(10)), pen)
def helper_ellipse(mode, bbox): @pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
def test_ellipse(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im) draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("blue", width=2) pen = ImageDraw2.Pen("blue", width=2)
brush = ImageDraw2.Brush("green") brush = ImageDraw2.Brush("green")
expected = f"Tests/images/imagedraw_ellipse_{mode}.png"
# Act # Act
draw.ellipse(bbox, pen, brush) draw.ellipse(bbox, pen, brush)
# Assert # Assert
assert_image_similar_tofile(im, expected, 1) assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1)
def test_ellipse1():
helper_ellipse("RGB", BBOX1)
def test_ellipse2():
helper_ellipse("RGB", BBOX2)
def test_ellipse_edge(): def test_ellipse_edge():
@ -88,7 +80,8 @@ def test_ellipse_edge():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im) draw = ImageDraw2.Draw(im)
@ -101,14 +94,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") 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(): def test_line_pen_as_brush():
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -124,7 +109,8 @@ def test_line_pen_as_brush():
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") 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 # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im) draw = ImageDraw2.Draw(im)
@ -138,15 +124,8 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
def test_polygon1(): @pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
helper_polygon(POINTS1) def test_rectangle(bbox):
def test_polygon2():
helper_polygon(POINTS2)
def helper_rectangle(bbox):
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im) draw = ImageDraw2.Draw(im)
@ -160,14 +139,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") 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(): def test_big_rectangle():
# Test drawing a rectangle bigger than the image # Test drawing a rectangle bigger than the image
# Arrange # Arrange

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image, ImageEnhance from PIL import Image, ImageEnhance
from .helper import assert_image_equal, hopper 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 # Issue https://github.com/python-pillow/Pillow/issues/899
# Is alpha preserved through image enhancement? # Is alpha preserved through image enhancement?
original = _half_transparent_image() original = _half_transparent_image()
for op in ["Color", "Brightness", "Contrast", "Sharpness"]: for amount in [0, 0.5, 1.0]:
for amount in [0, 0.5, 1.0]: _check_alpha(
_check_alpha( getattr(ImageEnhance, op)(original).enhance(amount),
getattr(ImageEnhance, op)(original).enhance(amount), original,
original, op,
op, amount,
amount, )
)

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,8 @@ from PIL import Image, ImageMath
def pixel(im): def pixel(im):
if hasattr(im, "im"): if hasattr(im, "im"):
return f"{im.mode} {repr(im.getpixel((0, 0)))}" return f"{im.mode} {repr(im.getpixel((0, 0)))}"
else: if isinstance(im, int):
if isinstance(im, int): return int(im) # hack to deal with booleans
return int(im) # hack to deal with booleans
print(im)
A = Image.new("L", (1, 1), 1) A = Image.new("L", (1, 1), 1)

View File

@ -65,14 +65,16 @@ def create_lut():
# create_lut() # create_lut()
def test_lut(): @pytest.mark.parametrize(
for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
lb = ImageMorph.LutBuilder(op_name=op) )
assert lb.get_lut() is None def test_lut(op):
lb = ImageMorph.LutBuilder(op_name=op)
assert lb.get_lut() is None
lut = lb.build_lut() lut = lb.build_lut()
with open(f"Tests/images/{op}.lut", "rb") as f: with open(f"Tests/images/{op}.lut", "rb") as f:
assert lut == bytearray(f.read()) assert lut == bytearray(f.read())
def test_no_operator_loaded(): def test_no_operator_loaded():

View File

@ -110,6 +110,16 @@ def test_contain(new_size):
assert new_im.size == (256, 256) 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(): def test_pad():
# Same ratio # Same ratio
im = hopper() 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(): def test_pil163():
# Division by zero in equalize if < 255 pixels in image (@PIL163) # Division by zero in equalize if < 255 pixels in image (@PIL163)

View File

@ -50,6 +50,16 @@ def test_getcolor():
palette.getcolor("unknown") palette.getcolor("unknown")
def test_getcolor_rgba_color_rgb_palette():
palette = ImagePalette.ImagePalette("RGB")
# Opaque RGBA colors are converted
assert palette.getcolor((0, 0, 0, 255)) == palette.getcolor((0, 0, 0))
with pytest.raises(ValueError):
palette.getcolor((0, 0, 0, 128))
@pytest.mark.parametrize( @pytest.mark.parametrize(
"index, palette", "index, palette",
[ [

View File

@ -45,10 +45,10 @@ def test_viewer_show(order):
not on_ci() or is_win32(), not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs", reason="Only run on CIs; hangs on Windows CIs",
) )
def test_show(): @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
for mode in ("1", "I;16", "LA", "RGB", "RGBA"): def test_show(mode):
im = hopper(mode) im = hopper(mode)
assert ImageShow.show(im) assert ImageShow.show(im)
def test_show_without_viewers(): def test_show_without_viewers():
@ -70,12 +70,12 @@ def test_viewer():
viewer.get_command(None) viewer.get_command(None)
def test_viewers(): @pytest.mark.parametrize("viewer", ImageShow._viewers)
for viewer in ImageShow._viewers: def test_viewers(viewer):
try: try:
viewer.get_command("test.jpg") viewer.get_command("test.jpg")
except NotImplementedError: except NotImplementedError:
pass pass
def test_ipythonviewer(): def test_ipythonviewer():
@ -95,14 +95,14 @@ def test_ipythonviewer():
not on_ci() or is_win32(), not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs", 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") f = str(tmp_path / "temp.jpg")
for viewer in ImageShow._viewers: hopper().save(f)
hopper().save(f) with pytest.warns(DeprecationWarning):
with pytest.warns(DeprecationWarning): try:
try: viewer.show_file(file=f)
viewer.show_file(file=f) except NotImplementedError:
except NotImplementedError: pass
pass with pytest.raises(TypeError):
with pytest.raises(TypeError): viewer.show_file()
viewer.show_file()

View File

@ -54,32 +54,39 @@ def test_kw():
assert im is None assert im is None
def test_photoimage(): @pytest.mark.parametrize("mode", TK_MODES)
for mode in TK_MODES: def test_photoimage(mode):
# test as image: # test as image:
im = hopper(mode) 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) im_tk = ImageTk.PhotoImage(im)
assert im_tk.width() == im.width
assert im_tk.height() == im.height
reloaded = ImageTk.getimage(im_tk) reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA")) 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: # 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.width() == 100
assert im_tk.height() == 100 assert im_tk.height() == 100
im = Image.new(mode, (100, 100)) im = Image.new(mode, (100, 100))
reloaded = ImageTk.getimage(im_tk) reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded.convert(mode), im) assert_image_equal(reloaded.convert(mode), im)
def test_box_deprecation(): def test_box_deprecation():

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image from PIL import Image
from .helper import hopper from .helper import hopper
@ -20,65 +22,56 @@ def verify(im1):
), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}" ), 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 # PIL 1.1 has limited support for 16-bit image data. Check that
# create/copy/transform and save works as expected. # create/copy/transform and save works as expected.
def basic(mode): im_in = original.convert(mode)
verify(im_in)
im_in = original.convert(mode) w, h = im_in.size
verify(im_in)
w, h = im_in.size im_out = im_in.copy()
verify(im_out) # copy
im_out = im_in.copy() im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(im_out) # copy verify(im_out) # transform
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) filename = str(tmp_path / "temp.im")
verify(im_out) # transform im_in.save(filename)
filename = str(tmp_path / "temp.im") with Image.open(filename) as im_out:
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))
verify(im_in) verify(im_in)
verify(im_out) verify(im_out)
im_in = Image.new(mode, (1, 1), 1) im_out = im_in.crop((0, 0, w, h))
assert im_in.getpixel((0, 0)) == 1 verify(im_out)
im_in.putpixel((0, 0), 2) im_out = Image.new(mode, (w, h), None)
assert im_in.getpixel((0, 0)) == 2 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": verify(im_in)
maximum = 255 verify(im_out)
else:
maximum = 32767
im_in = Image.new(mode, (1, 1), 256) im_in = Image.new(mode, (1, 1), 1)
assert im_in.getpixel((0, 0)) == min(256, maximum) assert im_in.getpixel((0, 0)) == 1
im_in.putpixel((0, 0), 512) im_in.putpixel((0, 0), 2)
assert im_in.getpixel((0, 0)) == min(512, maximum) assert im_in.getpixel((0, 0)) == 2
basic("L") if mode == "L":
maximum = 255
else:
maximum = 32767
basic("I;16") im_in = Image.new(mode, (1, 1), 256)
basic("I;16B") assert im_in.getpixel((0, 0)) == min(256, maximum)
basic("I;16L")
basic("I") im_in.putpixel((0, 0), 512)
assert im_in.getpixel((0, 0)) == min(512, maximum)
def test_tobytes(): def test_tobytes():

View File

@ -137,19 +137,9 @@ def test_save_tiff_uint16():
assert img_px[0, 0] == pixel_value assert img_px[0, 0] == pixel_value
def test_to_array(): @pytest.mark.parametrize(
def _to_array(mode, dtype): "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 = [
("L", numpy.uint8), ("L", numpy.uint8),
("I", numpy.int32), ("I", numpy.int32),
("F", numpy.float32), ("F", numpy.float32),
@ -163,10 +153,18 @@ def test_to_array():
("I;16B", ">u2"), ("I;16B", ">u2"),
("I;16L", "<u2"), ("I;16L", "<u2"),
("HSV", numpy.uint8), ("HSV", numpy.uint8),
] ),
)
def test_to_array(mode, dtype):
img = hopper(mode)
for mode in modes: # Resize to non-square
_to_array(*mode) 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
def test_point_lut(): def test_point_lut():

View File

@ -60,11 +60,11 @@ def helper_pickle_string(pickle, protocol, test_file, mode):
("Tests/images/itxt_chunks.png", None), ("Tests/images/itxt_chunks.png", None),
], ],
) )
def test_pickle_image(tmp_path, test_file, test_mode): @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_image(tmp_path, test_file, test_mode, protocol):
# Act / Assert # Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): helper_pickle_string(pickle, protocol, test_file, test_mode)
helper_pickle_string(pickle, protocol, test_file, test_mode) helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode)
helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode)
def test_pickle_la_mode_with_palette(tmp_path): def test_pickle_la_mode_with_palette(tmp_path):

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install libimagequant # install libimagequant
archive=libimagequant-4.0.2 archive=libimagequant-4.0.4
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -43,8 +43,7 @@ clean:
-rm -rf $(BUILDDIR)/* -rm -rf $(BUILDDIR)/*
install-sphinx: install-sphinx:
$(PYTHON) -c "import sphinx" > /dev/null 2>&1 || $(PYTHON) -m pip install sphinx $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile
$(PYTHON) -c "import furo" > /dev/null 2>&1 || $(PYTHON) -m pip install furo
html: html:
$(MAKE) install-sphinx $(MAKE) install-sphinx

View File

@ -178,6 +178,8 @@ Image.coerce_e
This undocumented method has been deprecated and will be removed in Pillow 10 This undocumented method has been deprecated and will be removed in Pillow 10
(2023-07-01). (2023-07-01).
.. _Font size and offset methods:
Font size and offset methods Font size and offset methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -197,6 +199,40 @@ Deprecated Use
:py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` :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 Removed features
---------------- ----------------
@ -253,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 We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe
vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
.. _FreeType: https://www.freetype.org .. _FreeType: https://freetype.org/
im.offset im.offset
~~~~~~~~~ ~~~~~~~~~

View File

@ -60,7 +60,10 @@ Pillow also provides limited support for a few additional modes, including:
* ``BGR;24`` (24-bit reversed true colour) * ``BGR;24`` (24-bit reversed true colour)
* ``BGR;32`` (32-bit reversed true colour) * ``BGR;32`` (32-bit reversed true colour)
However, Pillow doesnt support user-defined modes; if you need to handle band Apart from these additional modes, Pillow doesn't yet support multichannel
images with a depth of more than 8 bits per channel.
Pillow also doesnt support user-defined modes; if you need to handle band
combinations that are not listed above, use a sequence of Image objects. combinations that are not listed above, use a sequence of Image objects.
You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode`

View File

@ -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`` Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
images, and all types of ``BLP2`` images. images, and all types of ``BLP2`` images.
Saving
~~~~~~
Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method
can take the following keyword arguments: can take the following keyword arguments:
@ -42,15 +45,19 @@ BMP
^^^ ^^^
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``, Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding or ``RGB`` data. 16-colour images are read as ``P`` images.
is not supported. Support for reading 8-bit run-length encoding was added in Pillow Support for reading 8-bit run-length encoding was added in Pillow 9.1.0.
9.1.0. Support for reading 4-bit run-length encoding was added in Pillow 9.3.0.
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
**compression** **compression**
Set to ``bmp_rle`` if the file is run-length encoded. Set to 1 if the file is a 256-color run-length encoded image.
Set to 2 if the file is a 16-color run-length encoded image.
DDS DDS
^^^ ^^^
@ -78,6 +85,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 than leaving them in the original color space. The EPS driver can write images
in ``L``, ``RGB`` and ``CMYK`` modes. in ``L``, ``RGB`` and ``CMYK`` modes.
Loading
~~~~~~~
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load` 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 method with the following parameters to affect how Ghostscript renders the EPS
@ -134,6 +144,11 @@ To restore the default behavior, where ``P`` mode images are only converted to
from PIL import GifImagePlugin from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
.. _gif-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
@ -171,6 +186,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. ``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame.
.. _gif-saving:
Saving Saving
~~~~~~ ~~~~~~
@ -278,6 +295,11 @@ sets the following :py:attr:`~PIL.Image.Image.info` property:
ask for ``(512, 512, 2)``, the final value of ask for ``(512, 512, 2)``, the final value of
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). :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: The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**append_images** **append_images**
@ -292,6 +314,11 @@ ICO
ICO is used to store icons on Windows. The largest available icon is read. 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: The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**sizes** **sizes**
@ -337,6 +364,11 @@ their original size while loading them.
By default Pillow doesn't allow loading of truncated JPEG files, set By default Pillow doesn't allow loading of truncated JPEG files, set
:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this.
.. _jpeg-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method may set the following The :py:meth:`~PIL.Image.open` method may set the following
:py:attr:`~PIL.Image.Image.info` properties if available: :py:attr:`~PIL.Image.Image.info` properties if available:
@ -383,6 +415,10 @@ The :py:meth:`~PIL.Image.open` method may set the following
.. versionadded:: 7.1.0 .. versionadded:: 7.1.0
.. _jpeg-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options: The :py:meth:`~PIL.Image.Image.save` method supports the following options:
@ -464,6 +500,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), 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. 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: The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**offset** **offset**
@ -575,6 +616,11 @@ called.
By default Pillow doesn't allow loading of truncated PNG files, set By default Pillow doesn't allow loading of truncated PNG files, set
:data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this. :data:`.ImageFile.LOAD_TRUNCATED_IMAGES` to override this.
.. _png-opening:
Opening
~~~~~~~
The :py:func:`~PIL.Image.open` function sets the following The :py:func:`~PIL.Image.open` function sets the following
:py:attr:`~PIL.Image.Image.info` properties, when appropriate: :py:attr:`~PIL.Image.Image.info` properties, when appropriate:
@ -613,6 +659,11 @@ decompression bombs. Additionally, the total size of all of the text
chunks is limited to :data:`.PngImagePlugin.MAX_TEXT_MEMORY`, defaulting to chunks is limited to :data:`.PngImagePlugin.MAX_TEXT_MEMORY`, defaulting to
64MB. 64MB.
.. _png-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options: The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**optimize** **optimize**
@ -803,6 +854,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 :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and
random access is allowed. random access is allowed.
.. _spider-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following attributes: The :py:meth:`~PIL.Image.open` method sets the following attributes:
**format** **format**
@ -819,8 +875,10 @@ is provided for converting floating point data to byte data (mode ``L``)::
im = Image.open("image001.spi").convert2byte() 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 extension of SPIDER files may be any 3 alphanumeric characters. Therefore
the output format must be specified explicitly:: the output format must be specified explicitly::
@ -837,6 +895,29 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``,
``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and ``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and
run-length encoded TGAs. 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 TIFF
^^^^ ^^^^
@ -853,6 +934,11 @@ uncompressed files.
support for reading Packbits, LZW and JPEG compressed TIFFs support for reading Packbits, LZW and JPEG compressed TIFFs
without using libtiff. without using libtiff.
.. _tiff-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
@ -904,8 +990,10 @@ and can be accessed in any order.
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the ``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
last frame. last frame.
Saving Tiff Images .. _tiff-saving:
~~~~~~~~~~~~~~~~~~
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
@ -1017,6 +1105,11 @@ WebP
Pillow reads and writes WebP files. The specifics of Pillow's capabilities with Pillow reads and writes WebP files. The specifics of Pillow's capabilities with
this format are currently undocumented. this format are currently undocumented.
.. _webp-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method supports the following options: The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**lossless** **lossless**
@ -1040,7 +1133,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
the system WebP library was built with webpmux support. the system WebP library was built with webpmux support.
Saving sequences Saving sequences
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
.. note:: .. note::
@ -1155,6 +1248,11 @@ GBR
The GBR decoder reads GIMP brush files, version 1 and 2. The GBR decoder reads GIMP brush files, version 1 and 2.
.. _gbr-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
@ -1170,6 +1268,11 @@ GD
Pillow reads uncompressed GD2 files. Note that you must use Pillow reads uncompressed GD2 files. Note that you must use
:py:func:`PIL.GdImageFile.open` to read such a file. :py:func:`PIL.GdImageFile.open` to read such a file.
.. _gd-opening:
Opening
~~~~~~~
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
@ -1209,6 +1312,11 @@ 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 methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported. 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 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`` 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 argument is present and true, then all frames will be saved, and the following
@ -1308,6 +1416,11 @@ XPM
Pillow reads X pixmap files (mode ``P``) with 256 colors or less. 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 The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:
@ -1332,6 +1445,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 files, using either JPEG or HEX encoding depending on the image mode (and
whether JPEG support is available or not). whether JPEG support is available or not).
.. _pdf-saving:
Saving
~~~~~~
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**save_all** **save_all**

View File

@ -69,6 +69,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/Pillow/
:alt: Number of PyPI downloads :alt: Number of PyPI downloads
.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge
:target: https://bestpractices.coreinfrastructure.org/projects/6331
:alt: OpenSSF Best Practices
Overview Overview
======== ========

View File

@ -166,7 +166,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.0.2** * Pillow has been tested with libimagequant **2.6-4.0.4**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.
@ -184,7 +184,7 @@ Many of Pillow's features require external libraries:
loads libfribidi at runtime if it is installed. loads libfribidi at runtime if it is installed.
On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` On Windows this requires compiling FriBiDi and installing ``fribidi.dll``
into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs)
<https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-desktop-applications>`_ <https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-desktop-applications>`_
(``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected).
See `Build Options`_ to see how to build this version. 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. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime.

View File

@ -10,8 +10,8 @@ provide constants and clear-text names for various well-known EXIF tags.
.. py:data:: TAGS .. py:data:: TAGS
:type: dict :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: descriptive string names. For instance:
>>> from PIL.ExifTags import TAGS >>> from PIL.ExifTags import TAGS
>>> TAGS[0x010e] >>> TAGS[0x010e]
@ -20,9 +20,28 @@ provide constants and clear-text names for various well-known EXIF tags.
.. py:data:: GPSTAGS .. py:data:: GPSTAGS
:type: dict :type: dict
The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to The GPSTAGS dictionary maps 8-bit integer EXIF GPS enumerations to
descriptive string names. For instance: descriptive string names. For instance:
>>> from PIL.ExifTags import GPSTAGS >>> from PIL.ExifTags import GPSTAGS
>>> GPSTAGS[20] >>> GPSTAGS[20]
'GPSDestLatitude' 'GPSDestLatitude'
These values are also exposed as ``enum.IntEnum`` classes.
.. py:data:: Base
>>> from PIL.ExifTags import Base
>>> Base.ImageDescription.value
270
>>> Base(270).name
'ImageDescription'
.. py:data:: GPS
>>> from PIL.ExifTags import GPS
>>> GPS.GPSDestLatitude.value
20
>>> GPS(20).name
'GPSDestLatitude'

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