Merge branch 'main' into upload-fribidi
|
@ -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\
|
||||||
|
|
|
@ -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
|
@ -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"]
|
||||||
|
}
|
7
.github/workflows/cifuzz.yml
vendored
|
@ -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
|
||||||
|
|
8
.github/workflows/lint.yml
vendored
|
@ -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
|
||||||
|
|
3
.github/workflows/macos-install.sh
vendored
|
@ -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
|
||||||
|
|
4
.github/workflows/release-drafter.yml
vendored
|
@ -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:
|
||||||
|
|
6
.github/workflows/stale.yml
vendored
|
@ -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"
|
||||||
|
|
9
.github/workflows/test-cygwin.yml
vendored
|
@ -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'
|
||||||
|
|
||||||
|
|
6
.github/workflows/test-docker.yml
vendored
|
@ -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 }}
|
||||||
|
|
14
.github/workflows/test-mingw.yml
vendored
|
@ -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:
|
||||||
|
|
4
.github/workflows/test-valgrind.yml
vendored
|
@ -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:
|
||||||
|
|
||||||
|
|
45
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||||
|
|
22
.github/workflows/test.yml
vendored
|
@ -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:
|
||||||
|
|
5
.github/workflows/tidelift.yml
vendored
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
120
CHANGES.rst
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
3
Makefile
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
@ -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
BIN
Tests/images/bc6h.dds
Normal file
BIN
Tests/images/bc6h.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
Tests/images/bc6h_sf.dds
Normal file
BIN
Tests/images/bc6h_sf.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
Tests/images/blp/blp1_jpeg2.blp
Normal file
BIN
Tests/images/bw_gradient.imt
Normal file
BIN
Tests/images/child_ifd.tiff
Normal file
BIN
Tests/images/child_ifd_jpeg.tiff
Normal file
BIN
Tests/images/hopper_lzma.tif
Normal file
BIN
Tests/images/hopper_palette_chunk_second.fli
Normal file
BIN
Tests/images/hopper_webp.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
Tests/images/hopper_webp.tif
Normal file
BIN
Tests/images/mmap_error.bmp
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
Tests/images/no_palette_after_rgb.gif
Normal file
After Width: | Height: | Size: 101 B |
BIN
Tests/images/palette_not_needed_for_second_frame.gif
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
Tests/images/test_woff2.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
Tests/images/text_float_coord.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
Tests/images/text_float_coord_1_alt.png
Normal file
After Width: | Height: | Size: 807 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -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))
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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_",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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",
|
||||||
(
|
(
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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")
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)]),
|
)
|
||||||
)
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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}",
|
)
|
||||||
)
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
)
|
||||||
)
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
[
|
[
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
|
@ -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 doesn’t 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 doesn’t 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`
|
||||||
|
|
|
@ -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**
|
||||||
|
|
|
@ -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
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'
|
||||||
|
|