Merge branch 'main' into cross-prefix
|
@ -6,6 +6,7 @@ init:
|
||||||
# Uncomment previous line to get RDP access during the build.
|
# Uncomment previous line to get RDP access during the build.
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
EXECUTABLE: python.exe
|
EXECUTABLE: python.exe
|
||||||
TEST_OPTIONS:
|
TEST_OPTIONS:
|
||||||
DEPLOY: YES
|
DEPLOY: YES
|
||||||
|
@ -14,7 +15,7 @@ environment:
|
||||||
ARCHITECTURE: x86
|
ARCHITECTURE: x86
|
||||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||||
- PYTHON: C:/Python38-x64
|
- PYTHON: C:/Python38-x64
|
||||||
ARCHITECTURE: x64
|
ARCHITECTURE: AMD64
|
||||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==2.16.2
|
cibuildwheel==2.16.5
|
||||||
|
|
1
.ci/requirements-mypy.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mypy==1.7.1
|
14
.coveragerc
|
@ -2,15 +2,19 @@
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
# Regexes for lines to exclude from consideration
|
# Regexes for lines to exclude from consideration
|
||||||
exclude_lines =
|
exclude_also =
|
||||||
# Have to re-enable the standard pragma:
|
# Don't complain if non-runnable code isn't run
|
||||||
pragma: no cover
|
|
||||||
|
|
||||||
# Don't complain if non-runnable code isn't run:
|
|
||||||
if 0:
|
if 0:
|
||||||
if __name__ == .__main__.:
|
if __name__ == .__main__.:
|
||||||
# Don't complain about debug code
|
# Don't complain about debug code
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
|
# Don't complain about compatibility code for missing optional dependencies
|
||||||
|
except ImportError
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
@abc.abstractmethod
|
||||||
|
# Empty bodies in protocols or abstract methods
|
||||||
|
^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$
|
||||||
|
^\s*\.\.\.(\s*#.*)?$
|
||||||
|
|
||||||
[run]
|
[run]
|
||||||
omit =
|
omit =
|
||||||
|
|
6
.git-blame-ignore-revs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Flake8
|
||||||
|
8de95676e0fd89f2326b3953488ab66ff29cd2d0
|
||||||
|
# Format with Black
|
||||||
|
53a7e3500437a9fd5826bc04758f7116bd7e52dc
|
||||||
|
# Format the C code with ClangFormat
|
||||||
|
46b7e86bab79450ec0a2866c6c0c679afb659d17
|
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
||||||
tidelift: "pypi/Pillow"
|
tidelift: "pypi/pillow"
|
||||||
|
|
18
.github/problem-matchers/gcc.json
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "gcc-problem-matcher",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"message": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2
.github/release-drafter.yml
vendored
|
@ -13,6 +13,8 @@ categories:
|
||||||
label: "Removal"
|
label: "Removal"
|
||||||
- title: "Testing"
|
- title: "Testing"
|
||||||
label: "Testing"
|
label: "Testing"
|
||||||
|
- title: "Type hints"
|
||||||
|
label: "Type hints"
|
||||||
|
|
||||||
exclude-labels:
|
exclude-labels:
|
||||||
- "changelog: skip"
|
- "changelog: skip"
|
||||||
|
|
14
.github/workflows/docs.yml
vendored
|
@ -7,10 +7,12 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "src/PIL/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "src/PIL/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -37,16 +39,26 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
cache: pip
|
cache: pip
|
||||||
cache-dependency-path: ".ci/*.sh"
|
cache-dependency-path: |
|
||||||
|
".ci/*.sh"
|
||||||
|
"pyproject.toml"
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
|
- name: Cache libimagequant
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: cache-libimagequant
|
||||||
|
with:
|
||||||
|
path: ~/cache-libimagequant
|
||||||
|
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
run: |
|
run: |
|
||||||
.ci/install.sh
|
.ci/install.sh
|
||||||
env:
|
env:
|
||||||
GHA_PYTHON_VERSION: "3.x"
|
GHA_PYTHON_VERSION: "3.x"
|
||||||
|
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
|
2
.github/workflows/lint.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: pre-commit cache
|
- name: pre-commit cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
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') }}
|
||||||
|
|
11
.github/workflows/macos-install.sh
vendored
|
@ -2,7 +2,16 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
|
brew install \
|
||||||
|
freetype \
|
||||||
|
ghostscript \
|
||||||
|
libimagequant \
|
||||||
|
libjpeg \
|
||||||
|
libraqm \
|
||||||
|
libtiff \
|
||||||
|
little-cms2 \
|
||||||
|
openjpeg \
|
||||||
|
webp
|
||||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
|
||||||
# TODO Update condition when cffi supports 3.13
|
# TODO Update condition when cffi supports 3.13
|
||||||
|
|
2
.github/workflows/release-drafter.yml
vendored
|
@ -23,6 +23,6 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Drafts your next release notes as pull requests are merged into "main"
|
# Drafts your next release notes as pull requests are merged into "main"
|
||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v6
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
1
.github/workflows/system-info.py
vendored
|
@ -6,6 +6,7 @@ This sort of info is missing from GitHub Actions.
|
||||||
Requested here:
|
Requested here:
|
||||||
https://github.com/actions/virtual-environments/issues/79
|
https://github.com/actions/virtual-environments/issues/79
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
17
.github/workflows/test-cygwin.yml
vendored
|
@ -8,7 +8,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -16,7 +15,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -28,6 +26,9 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
@ -49,9 +50,8 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Cygwin
|
- name: Install Cygwin
|
||||||
uses: cygwin/cygwin-install-action@v4
|
uses: egor-tensin/setup-cygwin@v4
|
||||||
with:
|
with:
|
||||||
platform: x86_64
|
|
||||||
packages: >
|
packages: >
|
||||||
gcc-g++
|
gcc-g++
|
||||||
ghostscript
|
ghostscript
|
||||||
|
@ -71,6 +71,7 @@ jobs:
|
||||||
make
|
make
|
||||||
netpbm
|
netpbm
|
||||||
perl
|
perl
|
||||||
|
python39=3.9.16-1
|
||||||
python3${{ matrix.python-minor-version }}-cffi
|
python3${{ matrix.python-minor-version }}-cffi
|
||||||
python3${{ matrix.python-minor-version }}-cython
|
python3${{ matrix.python-minor-version }}-cython
|
||||||
python3${{ matrix.python-minor-version }}-devel
|
python3${{ matrix.python-minor-version }}-devel
|
||||||
|
@ -82,13 +83,13 @@ jobs:
|
||||||
zlib-devel
|
zlib-devel
|
||||||
|
|
||||||
- name: Add Lapack to PATH
|
- name: Add Lapack to PATH
|
||||||
uses: egor-tensin/cleanup-path@v3
|
uses: egor-tensin/cleanup-path@v4
|
||||||
with:
|
with:
|
||||||
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
||||||
|
|
||||||
- name: Select Python version
|
- name: Select Python version
|
||||||
run: |
|
run: |
|
||||||
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3
|
||||||
|
|
||||||
- name: Get latest NumPy version
|
- name: Get latest NumPy version
|
||||||
id: latest-numpy
|
id: latest-numpy
|
||||||
|
@ -97,7 +98,7 @@ jobs:
|
||||||
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
|
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: pip cache
|
- name: pip cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
||||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
|
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
|
||||||
|
@ -143,7 +144,7 @@ jobs:
|
||||||
bash.exe .ci/after_success.sh
|
bash.exe .ci/after_success.sh
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3.1.5
|
||||||
with:
|
with:
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
flags: GHA_Cygwin
|
flags: GHA_Cygwin
|
||||||
|
|
4
.github/workflows/test-docker.yml
vendored
|
@ -8,7 +8,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -16,7 +15,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -103,7 +101,7 @@ jobs:
|
||||||
MATRIX_DOCKER: ${{ matrix.docker }}
|
MATRIX_DOCKER: ${{ matrix.docker }}
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3.1.5
|
||||||
with:
|
with:
|
||||||
flags: GHA_Docker
|
flags: GHA_Docker
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
7
.github/workflows/test-mingw.yml
vendored
|
@ -8,7 +8,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -16,7 +15,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -28,6 +26,9 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
@ -84,7 +85,7 @@ 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
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3.1.5
|
||||||
with:
|
with:
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
flags: GHA_Windows
|
flags: GHA_Windows
|
||||||
|
|
25
.github/workflows/test-windows.yml
vendored
|
@ -2,11 +2,12 @@ name: Test Windows
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -14,7 +15,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -26,13 +26,16 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"]
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
@ -66,8 +69,16 @@ jobs:
|
||||||
- name: Print build system information
|
- name: Print build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
- name: Install Python dependencies
|
||||||
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
run: >
|
||||||
|
python3 -m pip install
|
||||||
|
coverage>=7.4.2
|
||||||
|
defusedxml
|
||||||
|
olefile
|
||||||
|
pyroma
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-timeout
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: install
|
id: install
|
||||||
|
@ -89,7 +100,7 @@ jobs:
|
||||||
|
|
||||||
- name: Cache build
|
- name: Cache build
|
||||||
id: build-cache
|
id: build-cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: winbuild\build
|
path: winbuild\build
|
||||||
key:
|
key:
|
||||||
|
@ -202,7 +213,7 @@ jobs:
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3.1.5
|
||||||
with:
|
with:
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
flags: GHA_Windows
|
flags: GHA_Windows
|
||||||
|
|
43
.github/workflows/test.yml
vendored
|
@ -8,7 +8,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
@ -16,7 +15,6 @@ on:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
- ".github/workflows/wheels*"
|
- ".github/workflows/wheels*"
|
||||||
- ".gitmodules"
|
- ".gitmodules"
|
||||||
- ".travis.yml"
|
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "wheels/**"
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -28,6 +26,10 @@ concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
|
@ -35,7 +37,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [
|
os: [
|
||||||
"macos-latest",
|
"macos-14",
|
||||||
"ubuntu-latest",
|
"ubuntu-latest",
|
||||||
]
|
]
|
||||||
python-version: [
|
python-version: [
|
||||||
|
@ -49,11 +51,21 @@ jobs:
|
||||||
"3.8",
|
"3.8",
|
||||||
]
|
]
|
||||||
include:
|
include:
|
||||||
- python-version: "3.9"
|
- python-version: "3.11"
|
||||||
PYTHONOPTIMIZE: 1
|
PYTHONOPTIMIZE: 1
|
||||||
REVERSE: "--reverse"
|
REVERSE: "--reverse"
|
||||||
- python-version: "3.8"
|
- python-version: "3.10"
|
||||||
PYTHONOPTIMIZE: 2
|
PYTHONOPTIMIZE: 2
|
||||||
|
# M1 only available for 3.10+
|
||||||
|
- os: "macos-latest"
|
||||||
|
python-version: "3.9"
|
||||||
|
- os: "macos-latest"
|
||||||
|
python-version: "3.8"
|
||||||
|
exclude:
|
||||||
|
- os: "macos-14"
|
||||||
|
python-version: "3.9"
|
||||||
|
- os: "macos-14"
|
||||||
|
python-version: "3.8"
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
|
@ -67,17 +79,28 @@ jobs:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
allow-prereleases: true
|
allow-prereleases: true
|
||||||
cache: pip
|
cache: pip
|
||||||
cache-dependency-path: ".ci/*.sh"
|
cache-dependency-path: |
|
||||||
|
".ci/*.sh"
|
||||||
|
"pyproject.toml"
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
|
- name: Cache libimagequant
|
||||||
|
if: startsWith(matrix.os, 'ubuntu')
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: cache-libimagequant
|
||||||
|
with:
|
||||||
|
path: ~/cache-libimagequant
|
||||||
|
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
if: startsWith(matrix.os, 'ubuntu')
|
if: startsWith(matrix.os, 'ubuntu')
|
||||||
run: |
|
run: |
|
||||||
.ci/install.sh
|
.ci/install.sh
|
||||||
env:
|
env:
|
||||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||||
|
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
|
||||||
|
|
||||||
- name: Install macOS dependencies
|
- name: Install macOS dependencies
|
||||||
if: startsWith(matrix.os, 'macOS')
|
if: startsWith(matrix.os, 'macOS')
|
||||||
|
@ -86,6 +109,10 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Register gcc problem matcher
|
||||||
|
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
|
||||||
|
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
.ci/build.sh
|
.ci/build.sh
|
||||||
|
@ -123,9 +150,9 @@ jobs:
|
||||||
.ci/after_success.sh
|
.ci/after_success.sh
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3.1.5
|
||||||
with:
|
with:
|
||||||
flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }}
|
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
gcov: true
|
gcov: true
|
||||||
|
|
||||||
|
|
6
.github/workflows/wheels-dependencies.sh
vendored
|
@ -72,14 +72,12 @@ function build {
|
||||||
|
|
||||||
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||||
if [ -n "$IS_MACOS" ]; then
|
if [ -n "$IS_MACOS" ]; then
|
||||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
|
||||||
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
||||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||||
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
|
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
|
||||||
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
|
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||||
fi
|
fi
|
||||||
|
@ -131,13 +129,13 @@ untar pillow-depends-main.zip
|
||||||
|
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
|
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
|
||||||
# libxdmcp causes an issue on macOS < 11
|
# libxau and libxdmcp cause an issue on macOS < 11
|
||||||
# if php is installed, brew tries to reinstall these after installing openblas
|
# if php is installed, brew tries to reinstall these after installing openblas
|
||||||
# remove cairo to fix building harfbuzz on arm64
|
# remove cairo to fix building harfbuzz on arm64
|
||||||
# remove lcms2 and libpng to fix building openjpeg on arm64
|
# remove lcms2 and libpng to fix building openjpeg on arm64
|
||||||
# remove zstd to avoid inclusion on x86_64
|
# remove zstd to avoid inclusion on x86_64
|
||||||
# curl from brew requires zstd, use system curl
|
# curl from brew requires zstd, use system curl
|
||||||
brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd
|
brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd
|
||||||
|
|
||||||
brew install pkg-config
|
brew install pkg-config
|
||||||
fi
|
fi
|
||||||
|
|
146
.github/workflows/wheels.yml
vendored
|
@ -30,7 +30,64 @@ env:
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-1-QEMU-emulated-wheels:
|
||||||
|
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- pp39
|
||||||
|
- pp310
|
||||||
|
- cp38
|
||||||
|
- cp39
|
||||||
|
- cp310
|
||||||
|
- cp311
|
||||||
|
- cp312
|
||||||
|
spec:
|
||||||
|
- manylinux2014
|
||||||
|
- manylinux_2_28
|
||||||
|
- musllinux
|
||||||
|
exclude:
|
||||||
|
- { python-version: pp39, spec: musllinux }
|
||||||
|
- { python-version: pp310, spec: musllinux }
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
# https://github.com/docker/setup-qemu-action
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Install cibuildwheel
|
||||||
|
run: |
|
||||||
|
python3 -m pip install -r .ci/requirements-cibw.txt
|
||||||
|
|
||||||
|
- name: Build wheels
|
||||||
|
run: |
|
||||||
|
python3 -m cibuildwheel --output-dir wheelhouse
|
||||||
|
env:
|
||||||
|
# Build only the currently selected Linux architecture (so we can
|
||||||
|
# parallelise for speed).
|
||||||
|
CIBW_ARCHS: "aarch64"
|
||||||
|
# Likewise, select only one Python version per job to speed this up.
|
||||||
|
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
|
||||||
|
# Extra options for manylinux.
|
||||||
|
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||||
|
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
|
||||||
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
|
build-2-native-wheels:
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -39,18 +96,18 @@ jobs:
|
||||||
include:
|
include:
|
||||||
- name: "macOS x86_64"
|
- name: "macOS x86_64"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
archs: x86_64
|
cibw_arch: x86_64
|
||||||
macosx_deployment_target: "10.10"
|
macosx_deployment_target: "10.10"
|
||||||
- name: "macOS arm64"
|
- name: "macOS arm64"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
archs: arm64
|
cibw_arch: arm64
|
||||||
macosx_deployment_target: "11.0"
|
macosx_deployment_target: "11.0"
|
||||||
- name: "manylinux2014 and musllinux x86_64"
|
- name: "manylinux2014 and musllinux x86_64"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
archs: x86_64
|
cibw_arch: x86_64
|
||||||
- name: "manylinux_2_28 x86_64"
|
- name: "manylinux_2_28 x86_64"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
archs: x86_64
|
cibw_arch: x86_64
|
||||||
build: "*manylinux*"
|
build: "*manylinux*"
|
||||||
manylinux: "manylinux_2_28"
|
manylinux: "manylinux_2_28"
|
||||||
steps:
|
steps:
|
||||||
|
@ -62,12 +119,15 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Install cibuildwheel
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install -r .ci/requirements-cibw.txt
|
python3 -m pip install -r .ci/requirements-cibw.txt
|
||||||
|
|
||||||
|
- name: Build wheels
|
||||||
|
run: |
|
||||||
python3 -m cibuildwheel --output-dir wheelhouse
|
python3 -m cibuildwheel --output-dir wheelhouse
|
||||||
env:
|
env:
|
||||||
CIBW_ARCHS: ${{ matrix.archs }}
|
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||||
CIBW_BUILD: ${{ matrix.build }}
|
CIBW_BUILD: ${{ matrix.build }}
|
||||||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||||
|
@ -75,24 +135,21 @@ jobs:
|
||||||
CIBW_TEST_SKIP: "*-macosx_arm64"
|
CIBW_TEST_SKIP: "*-macosx_arm64"
|
||||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
name: Windows ${{ matrix.arch }}
|
name: Windows ${{ matrix.cibw_arch }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- arch: x86
|
- cibw_arch: x86
|
||||||
cibw_arch: x86
|
- cibw_arch: AMD64
|
||||||
- arch: x64
|
- cibw_arch: ARM64
|
||||||
cibw_arch: AMD64
|
|
||||||
- arch: ARM64
|
|
||||||
cibw_arch: ARM64
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
@ -106,6 +163,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Install cibuildwheel
|
||||||
|
run: |
|
||||||
|
python.exe -m pip install -r .ci/requirements-cibw.txt
|
||||||
|
|
||||||
- name: Prepare for build
|
- name: Prepare for build
|
||||||
run: |
|
run: |
|
||||||
choco install nasm --no-progress
|
choco install nasm --no-progress
|
||||||
|
@ -114,12 +175,7 @@ jobs:
|
||||||
# Install extra test images
|
# Install extra test images
|
||||||
xcopy /S /Y Tests\test-images\* Tests\images
|
xcopy /S /Y Tests\test-images\* Tests\images
|
||||||
|
|
||||||
& python.exe -m pip install -r .ci/requirements-cibw.txt
|
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
|
||||||
|
|
||||||
# Cannot cross-compile FriBiDi (only used for tests)
|
|
||||||
$FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
|
|
||||||
if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
|
|
||||||
& python.exe winbuild\build_prepare.py -v @FLAGS
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
|
@ -146,6 +202,7 @@ jobs:
|
||||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||||
CIBW_CACHE_PATH: "C:\\cibw"
|
CIBW_CACHE_PATH: "C:\\cibw"
|
||||||
|
CIBW_SKIP: pp38-*
|
||||||
CIBW_TEST_SKIP: "*-win_arm64"
|
CIBW_TEST_SKIP: "*-win_arm64"
|
||||||
CIBW_TEST_COMMAND: 'docker run --rm
|
CIBW_TEST_COMMAND: 'docker run --rm
|
||||||
-v {project}:C:\pillow
|
-v {project}:C:\pillow
|
||||||
|
@ -157,24 +214,16 @@ jobs:
|
||||||
shell: cmd
|
shell: cmd
|
||||||
|
|
||||||
- name: Upload wheels
|
- name: Upload wheels
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist-windows-${{ matrix.cibw_arch }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
- name: Prepare to upload FriBiDi
|
|
||||||
if: "matrix.arch != 'ARM64'"
|
|
||||||
run: |
|
|
||||||
mkdir fribidi\${{ matrix.arch }}
|
|
||||||
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
|
|
||||||
shell: cmd
|
|
||||||
|
|
||||||
- name: Upload fribidi.dll
|
- name: Upload fribidi.dll
|
||||||
if: "matrix.arch != 'ARM64'"
|
uses: actions/upload-artifact@v4
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
with:
|
||||||
name: fribidi
|
name: fribidi-windows-${{ matrix.cibw_arch }}
|
||||||
path: fribidi\*
|
path: winbuild\build\bin\fribidi*
|
||||||
|
|
||||||
sdist:
|
sdist:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -190,17 +239,26 @@ jobs:
|
||||||
|
|
||||||
- run: make sdist
|
- run: make sdist
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist-sdist
|
||||||
path: dist/*.tar.gz
|
path: dist/*.tar.gz
|
||||||
|
|
||||||
success:
|
pypi-publish:
|
||||||
permissions:
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||||
contents: none
|
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
|
||||||
needs: [build, windows, sdist]
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Wheels Successful
|
name: Upload release to PyPI
|
||||||
|
environment:
|
||||||
|
name: release-pypi
|
||||||
|
url: https://pypi.org/p/Pillow
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Success
|
- uses: actions/download-artifact@v4
|
||||||
run: echo Wheels Successful
|
with:
|
||||||
|
pattern: dist-*
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Publish to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.1.7
|
rev: v0.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [--fix, --exit-non-zero-on-fix]
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 23.12.0
|
rev: 24.1.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
rev: 1.7.6
|
rev: 1.7.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: bandit
|
- id: bandit
|
||||||
args: [--severity-level=high]
|
args: [--severity-level=high]
|
||||||
|
@ -32,6 +32,7 @@ repos:
|
||||||
rev: v4.5.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
|
- id: check-shebang-scripts-are-executable
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
|
@ -47,12 +48,12 @@ repos:
|
||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: 1.5.3
|
rev: 1.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
|
|
||||||
- repo: https://github.com/abravalheri/validate-pyproject
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
rev: v0.15
|
rev: v0.16
|
||||||
hooks:
|
hooks:
|
||||||
- id: validate-pyproject
|
- id: validate-pyproject
|
||||||
|
|
||||||
|
|
52
.travis.yml
|
@ -1,52 +0,0 @@
|
||||||
if: tag IS present OR type = api
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- CIBW_ARCHS=aarch64
|
|
||||||
- CIBW_SKIP=pp38-*
|
|
||||||
|
|
||||||
language: python
|
|
||||||
# Default Python version is usually 3.6
|
|
||||||
python: "3.12"
|
|
||||||
dist: jammy
|
|
||||||
services: docker
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- name: "manylinux2014 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- CIBW_BUILD="*manylinux*"
|
|
||||||
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
|
|
||||||
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
|
|
||||||
- name: "manylinux_2_28 aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- CIBW_BUILD="*manylinux*"
|
|
||||||
- CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
|
|
||||||
- CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
|
|
||||||
- name: "musllinux aarch64"
|
|
||||||
os: linux
|
|
||||||
arch: arm64
|
|
||||||
env:
|
|
||||||
- CIBW_BUILD="*musllinux*"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- python3 -m pip install -r .ci/requirements-cibw.txt
|
|
||||||
|
|
||||||
script:
|
|
||||||
- python3 -m cibuildwheel --output-dir wheelhouse
|
|
||||||
- ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
|
|
||||||
|
|
||||||
# Upload wheels to GitHub Releases
|
|
||||||
deploy:
|
|
||||||
provider: releases
|
|
||||||
api_key: $GITHUB_RELEASE_TOKEN
|
|
||||||
file_glob: true
|
|
||||||
file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
|
|
||||||
on:
|
|
||||||
repo: python-pillow/Pillow
|
|
||||||
tags: true
|
|
||||||
skip_cleanup: true
|
|
83
CHANGES.rst
|
@ -2,9 +2,90 @@
|
||||||
Changelog (Pillow)
|
Changelog (Pillow)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
10.2.0 (unreleased)
|
10.3.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
|
||||||
|
[evanmiller, radarhere]
|
||||||
|
|
||||||
|
- Fixed reading FLI/FLC images with a prefix chunk #7804
|
||||||
|
[twolife]
|
||||||
|
|
||||||
|
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
|
||||||
|
[nik012003, radarhere]
|
||||||
|
|
||||||
|
- Remove execute bit from ``setup.py`` #7760
|
||||||
|
[hugovk]
|
||||||
|
|
||||||
|
- Do not support using test-image-results to upload images after test failures #7739
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Changed ImageMath.ops to be static #7721
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fix APNG info after seeking backwards more than twice #7701
|
||||||
|
[esoma, radarhere]
|
||||||
|
|
||||||
|
- Deprecate ImageCms constants and versions() function #7702
|
||||||
|
[nulano, radarhere]
|
||||||
|
|
||||||
|
- Added PerspectiveTransform #7699
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Add support for reading and writing grayscale PFM images #7696
|
||||||
|
[nulano, hugovk]
|
||||||
|
|
||||||
|
- Add LCMS2 flags to ImageCms #7676
|
||||||
|
[nulano, radarhere, hugovk]
|
||||||
|
|
||||||
|
- Rename x64 to AMD64 in winbuild #7693
|
||||||
|
[nulano]
|
||||||
|
|
||||||
|
10.2.0 (2024-01-02)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553
|
||||||
|
[bgilbert, radarhere]
|
||||||
|
|
||||||
|
- Trim glyph size in ImageFont.getmask() #7669, #7672
|
||||||
|
[radarhere, nulano]
|
||||||
|
|
||||||
|
- Deprecate IptcImagePlugin helpers #7664
|
||||||
|
[nulano, hugovk, radarhere]
|
||||||
|
|
||||||
|
- Allow uncompressed TIFF images to be saved in chunks #7650
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Concatenate multiple JPEG EXIF markers #7496
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Changed IPTC tile tuple to match other plugins #7661
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not assign new fp attribute when exiting context manager #7566
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support arbitrary masks for uncompressed RGB DDS images #7589
|
||||||
|
[radarhere, akx]
|
||||||
|
|
||||||
|
- Support setting ROWSPERSTRIP tag #7654
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657
|
||||||
|
[hugovk]
|
||||||
|
|
||||||
|
- Restricted environment keys for ImageMath.eval() #7655
|
||||||
|
[wiredfool, radarhere]
|
||||||
|
|
||||||
|
- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
- Fix incorrect color blending for overlapping glyphs #7497
|
||||||
|
[ZachNagengast, nulano, radarhere]
|
||||||
|
|
||||||
- Attempt memory mapping when tile args is a string #7565
|
- Attempt memory mapping when tile args is a string #7565
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
2
LICENSE
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
||||||
|
|
||||||
Pillow is the friendly PIL fork. It is
|
Pillow is the friendly PIL fork. It is
|
||||||
|
|
||||||
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors.
|
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
|
||||||
|
|
||||||
Like PIL, Pillow is licensed under the open source HPND License:
|
Like PIL, Pillow is licensed under the open source HPND License:
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,6 @@ As of 2019, Pillow development is
|
||||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
|
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
|
||||||
alt="GitHub Actions build status (Wheels)"
|
alt="GitHub Actions build status (Wheels)"
|
||||||
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
|
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
|
||||||
<a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
|
|
||||||
alt="Travis CI wheels build status (aarch64)"
|
|
||||||
src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
|
|
||||||
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
|
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
|
||||||
alt="Code coverage"
|
alt="Code coverage"
|
||||||
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
|
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
|
||||||
|
@ -67,11 +64,11 @@ As of 2019, Pillow development is
|
||||||
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
||||||
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
||||||
alt="Tidelift"
|
alt="Tidelift"
|
||||||
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
|
src="https://tidelift.com/badges/package/pypi/pillow?style=flat"></a>
|
||||||
<a href="https://pypi.org/project/Pillow/"><img
|
<a href="https://pypi.org/project/pillow/"><img
|
||||||
alt="Newest PyPI version"
|
alt="Newest PyPI version"
|
||||||
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
||||||
<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://www.bestpractices.dev/projects/6331"><img
|
<a href="https://www.bestpractices.dev/projects/6331"><img
|
||||||
|
|
33
RELEASING.md
|
@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
||||||
* [ ] Develop and prepare release in `main` branch.
|
* [ ] Develop and prepare release in `main` branch.
|
||||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
||||||
* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them.
|
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
|
||||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||||
* [ ] Update `CHANGES.rst`.
|
* [ ] Update `CHANGES.rst`.
|
||||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||||
|
@ -20,12 +20,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
git tag 5.2.0
|
git tag 5.2.0
|
||||||
git push --tags
|
git push --tags
|
||||||
```
|
```
|
||||||
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||||
* [ ] Check and upload all source and binary distributions e.g.:
|
|
||||||
```bash
|
|
||||||
python3 -m twine check --strict dist/*
|
|
||||||
python3 -m twine upload dist/Pillow-5.2.0*
|
|
||||||
```
|
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
|
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
|
||||||
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
|
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
|
||||||
|
@ -55,12 +50,7 @@ Released as needed for security, installation or critical bug fixes.
|
||||||
```bash
|
```bash
|
||||||
make sdist
|
make sdist
|
||||||
```
|
```
|
||||||
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||||
* [ ] Check and upload all source and binary distributions e.g.:
|
|
||||||
```bash
|
|
||||||
python3 -m twine check --strict dist/*
|
|
||||||
python3 -m twine upload dist/Pillow-5.2.1*
|
|
||||||
```
|
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||||
```bash
|
```bash
|
||||||
git push
|
git push
|
||||||
|
@ -82,11 +72,7 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
git tag 2.5.3
|
git tag 2.5.3
|
||||||
git push origin --tags
|
git push origin --tags
|
||||||
```
|
```
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||||
```bash
|
|
||||||
make sdist
|
|
||||||
```
|
|
||||||
* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
|
||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||||
```bash
|
```bash
|
||||||
git push origin 2.5.x
|
git push origin 2.5.x
|
||||||
|
@ -94,14 +80,9 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
|
|
||||||
## Source and Binary Distributions
|
## Source and Binary Distributions
|
||||||
|
|
||||||
* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||||
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
|
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||||
```bash
|
by the new tag.
|
||||||
gh run download --dir dist
|
|
||||||
# select dist
|
|
||||||
```
|
|
||||||
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
|
|
||||||
and copy into `dist`.
|
|
||||||
|
|
||||||
## Publicize Release
|
## Publicize Release
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from PIL import PyAccess
|
from PIL import PyAccess
|
||||||
|
@ -8,21 +9,21 @@ from .helper import hopper
|
||||||
# Not running this test by default. No DOS against CI.
|
# Not running this test by default. No DOS against CI.
|
||||||
|
|
||||||
|
|
||||||
def iterate_get(size, access):
|
def iterate_get(size, access) -> None:
|
||||||
(w, h) = size
|
(w, h) = size
|
||||||
for x in range(w):
|
for x in range(w):
|
||||||
for y in range(h):
|
for y in range(h):
|
||||||
access[(x, y)]
|
access[(x, y)]
|
||||||
|
|
||||||
|
|
||||||
def iterate_set(size, access):
|
def iterate_set(size, access) -> None:
|
||||||
(w, h) = size
|
(w, h) = size
|
||||||
for x in range(w):
|
for x in range(w):
|
||||||
for y in range(h):
|
for y in range(h):
|
||||||
access[(x, y)] = (x % 256, y % 256, 0)
|
access[(x, y)] = (x % 256, y % 256, 0)
|
||||||
|
|
||||||
|
|
||||||
def timer(func, label, *args):
|
def timer(func, label, *args) -> None:
|
||||||
iterations = 5000
|
iterations = 5000
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
for x in range(iterations):
|
for x in range(iterations):
|
||||||
|
@ -37,7 +38,7 @@ def timer(func, label, *args):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_direct():
|
def test_direct() -> None:
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.load()
|
im.load()
|
||||||
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
|
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
TEST_FILE = "Tests/images/fli_overflow.fli"
|
TEST_FILE = "Tests/images/fli_overflow.fli"
|
||||||
|
|
||||||
|
|
||||||
def test_fli_overflow():
|
def test_fli_overflow() -> None:
|
||||||
# this should not crash with a malloc error or access violation
|
# this should not crash with a malloc error or access violation
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
@ -12,31 +15,34 @@ max_iterations = 10000
|
||||||
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
||||||
|
|
||||||
|
|
||||||
def _get_mem_usage():
|
def _get_mem_usage() -> float:
|
||||||
from resource import RUSAGE_SELF, getpagesize, getrusage
|
from resource import RUSAGE_SELF, getpagesize, getrusage
|
||||||
|
|
||||||
mem = getrusage(RUSAGE_SELF).ru_maxrss
|
mem = getrusage(RUSAGE_SELF).ru_maxrss
|
||||||
return mem * getpagesize() / 1024 / 1024
|
return mem * getpagesize() / 1024 / 1024
|
||||||
|
|
||||||
|
|
||||||
def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs):
|
def _test_leak(
|
||||||
|
min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
|
||||||
|
) -> None:
|
||||||
mem_limit = None
|
mem_limit = None
|
||||||
for i in range(max_iterations):
|
for i in range(max_iterations):
|
||||||
fn(*args, **kwargs)
|
fn(*args)
|
||||||
mem = _get_mem_usage()
|
mem = _get_mem_usage()
|
||||||
if i < min_iterations:
|
if i < min_iterations:
|
||||||
mem_limit = mem + 1
|
mem_limit = mem + 1
|
||||||
continue
|
continue
|
||||||
msg = f"memory usage limit exceeded after {i + 1} iterations"
|
msg = f"memory usage limit exceeded after {i + 1} iterations"
|
||||||
|
assert mem_limit is not None
|
||||||
assert mem <= mem_limit, msg
|
assert mem <= mem_limit, msg
|
||||||
|
|
||||||
|
|
||||||
def test_leak_putdata():
|
def test_leak_putdata() -> None:
|
||||||
im = Image.new("RGB", (25, 25))
|
im = Image.new("RGB", (25, 25))
|
||||||
_test_leak(min_iterations, max_iterations, im.putdata, im.getdata())
|
_test_leak(min_iterations, max_iterations, im.putdata, im.getdata())
|
||||||
|
|
||||||
|
|
||||||
def test_leak_getlist():
|
def test_leak_getlist() -> None:
|
||||||
im = Image.new("P", (25, 25))
|
im = Image.new("P", (25, 25))
|
||||||
_test_leak(
|
_test_leak(
|
||||||
min_iterations,
|
min_iterations,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -19,7 +20,7 @@ pytestmark = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_leak_load():
|
def test_leak_load() -> None:
|
||||||
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
||||||
|
|
||||||
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
||||||
|
@ -29,7 +30,7 @@ def test_leak_load():
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_leak_save():
|
def test_leak_save() -> None:
|
||||||
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
|
||||||
|
|
||||||
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
def test_j2k_overflow(tmp_path):
|
def test_j2k_overflow(tmp_path: Path) -> None:
|
||||||
im = Image.new("RGBA", (1024, 131584))
|
im = Image.new("RGBA", (1024, 131584))
|
||||||
target = str(tmp_path / "temp.jpc")
|
target = str(tmp_path / "temp.jpc")
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
3
Tests/check_jp2_overflow.py
Executable file → Normal file
|
@ -1,5 +1,3 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Reproductions/tests for OOB read errors in FliDecode.c
|
# Reproductions/tests for OOB read errors in FliDecode.c
|
||||||
|
|
||||||
# When run in python, all of these images should fail for
|
# When run in python, all of these images should fail for
|
||||||
|
@ -14,7 +12,6 @@
|
||||||
# version.
|
# version.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")
|
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -110,14 +111,14 @@ standard_chrominance_qtable = (
|
||||||
[standard_l_qtable, standard_chrominance_qtable],
|
[standard_l_qtable, standard_chrominance_qtable],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_qtables_leak(qtables):
|
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
test_output = BytesIO()
|
test_output = BytesIO()
|
||||||
im.save(test_output, "JPEG", qtables=qtables)
|
im.save(test_output, "JPEG", qtables=qtables)
|
||||||
|
|
||||||
|
|
||||||
def test_exif_leak():
|
def test_exif_leak() -> None:
|
||||||
"""
|
"""
|
||||||
pre patch:
|
pre patch:
|
||||||
|
|
||||||
|
@ -180,7 +181,7 @@ def test_exif_leak():
|
||||||
im.save(test_output, "JPEG", exif=exif)
|
im.save(test_output, "JPEG", exif=exif)
|
||||||
|
|
||||||
|
|
||||||
def test_base_save():
|
def test_base_save() -> None:
|
||||||
"""
|
"""
|
||||||
base case:
|
base case:
|
||||||
MB
|
MB
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -15,6 +18,7 @@ from PIL import Image
|
||||||
# 2.7 and 3.2.
|
# 2.7 and 3.2.
|
||||||
|
|
||||||
|
|
||||||
|
numpy: ModuleType | None
|
||||||
try:
|
try:
|
||||||
import numpy
|
import numpy
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -27,23 +31,24 @@ XDIM = 48000
|
||||||
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
|
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
|
||||||
|
|
||||||
|
|
||||||
def _write_png(tmp_path, xdim, ydim):
|
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
|
||||||
f = str(tmp_path / "temp.png")
|
f = str(tmp_path / "temp.png")
|
||||||
im = Image.new("L", (xdim, ydim), 0)
|
im = Image.new("L", (xdim, ydim), 0)
|
||||||
im.save(f)
|
im.save(f)
|
||||||
|
|
||||||
|
|
||||||
def test_large(tmp_path):
|
def test_large(tmp_path: Path) -> None:
|
||||||
"""succeeded prepatch"""
|
"""succeeded prepatch"""
|
||||||
_write_png(tmp_path, XDIM, YDIM)
|
_write_png(tmp_path, XDIM, YDIM)
|
||||||
|
|
||||||
|
|
||||||
def test_2gpx(tmp_path):
|
def test_2gpx(tmp_path: Path) -> None:
|
||||||
"""failed prepatch"""
|
"""failed prepatch"""
|
||||||
_write_png(tmp_path, XDIM, XDIM)
|
_write_png(tmp_path, XDIM, XDIM)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(numpy is None, reason="Numpy is not installed")
|
@pytest.mark.skipif(numpy is None, reason="Numpy is not installed")
|
||||||
def test_size_greater_than_int():
|
def test_size_greater_than_int() -> None:
|
||||||
|
assert numpy is not None
|
||||||
arr = numpy.ndarray(shape=(16394, 16394))
|
arr = numpy.ndarray(shape=(16394, 16394))
|
||||||
Image.fromarray(arr)
|
Image.fromarray(arr)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -23,7 +25,7 @@ XDIM = 48000
|
||||||
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
|
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
|
||||||
|
|
||||||
|
|
||||||
def _write_png(tmp_path, xdim, ydim):
|
def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
|
||||||
dtype = np.uint8
|
dtype = np.uint8
|
||||||
a = np.zeros((xdim, ydim), dtype=dtype)
|
a = np.zeros((xdim, ydim), dtype=dtype)
|
||||||
f = str(tmp_path / "temp.png")
|
f = str(tmp_path / "temp.png")
|
||||||
|
@ -31,11 +33,11 @@ def _write_png(tmp_path, xdim, ydim):
|
||||||
im.save(f)
|
im.save(f)
|
||||||
|
|
||||||
|
|
||||||
def test_large(tmp_path):
|
def test_large(tmp_path: Path) -> None:
|
||||||
"""succeeded prepatch"""
|
"""succeeded prepatch"""
|
||||||
_write_png(tmp_path, XDIM, YDIM)
|
_write_png(tmp_path, XDIM, YDIM)
|
||||||
|
|
||||||
|
|
||||||
def test_2gpx(tmp_path):
|
def test_2gpx(tmp_path: Path) -> None:
|
||||||
"""failed prepatch"""
|
"""failed prepatch"""
|
||||||
_write_png(tmp_path, XDIM, XDIM)
|
_write_png(tmp_path, XDIM, XDIM)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
@ -6,7 +7,7 @@ from PIL import Image
|
||||||
TEST_FILE = "Tests/images/libtiff_segfault.tif"
|
TEST_FILE = "Tests/images/libtiff_segfault.tif"
|
||||||
|
|
||||||
|
|
||||||
def test_libtiff_segfault():
|
def test_libtiff_segfault() -> None:
|
||||||
"""This test should not segfault. It will on Pillow <= 3.1.0 and
|
"""This test should not segfault. It will on Pillow <= 3.1.0 and
|
||||||
libtiff >= 4.0.0
|
libtiff >= 4.0.0
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import zlib
|
import zlib
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
@ -7,7 +8,7 @@ from PIL import Image, ImageFile, PngImagePlugin
|
||||||
TEST_FILE = "Tests/images/png_decompression_dos.png"
|
TEST_FILE = "Tests/images/png_decompression_dos.png"
|
||||||
|
|
||||||
|
|
||||||
def test_ignore_dos_text():
|
def test_ignore_dos_text() -> None:
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -23,7 +24,7 @@ def test_ignore_dos_text():
|
||||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||||
|
|
||||||
|
|
||||||
def test_dos_text():
|
def test_dos_text() -> None:
|
||||||
try:
|
try:
|
||||||
im = Image.open(TEST_FILE)
|
im = Image.open(TEST_FILE)
|
||||||
im.load()
|
im.load()
|
||||||
|
@ -35,7 +36,7 @@ def test_dos_text():
|
||||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||||
|
|
||||||
|
|
||||||
def test_dos_total_memory():
|
def test_dos_total_memory() -> None:
|
||||||
im = Image.new("L", (1, 1))
|
im = Image.new("L", (1, 1))
|
||||||
compressed_data = zlib.compress(b"a" * 1024 * 1023)
|
compressed_data = zlib.compress(b"a" * 1024 * 1023)
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ def test_dos_total_memory():
|
||||||
try:
|
try:
|
||||||
im2 = Image.open(b)
|
im2 = Image.open(b)
|
||||||
except ValueError as msg:
|
except ValueError as msg:
|
||||||
assert "Too much memory" in msg
|
assert "Too much memory" in str(msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
total_len = 0
|
total_len = 0
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PIL import features
|
from PIL import features
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_modules():
|
def test_wheel_modules() -> None:
|
||||||
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
|
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
|
||||||
|
|
||||||
# tkinter is not available in cibuildwheel installed CPython on Windows
|
# tkinter is not available in cibuildwheel installed CPython on Windows
|
||||||
|
@ -18,13 +19,13 @@ def test_wheel_modules():
|
||||||
assert set(features.get_supported_modules()) == expected_modules
|
assert set(features.get_supported_modules()) == expected_modules
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_codecs():
|
def test_wheel_codecs() -> None:
|
||||||
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
|
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
|
||||||
|
|
||||||
assert set(features.get_supported_codecs()) == expected_codecs
|
assert set(features.get_supported_codecs()) == expected_codecs
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_features():
|
def test_wheel_features() -> None:
|
||||||
expected_features = {
|
expected_features = {
|
||||||
"webp_anim",
|
"webp_anim",
|
||||||
"webp_mux",
|
"webp_mux",
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
def pytest_report_header(config):
|
|
||||||
|
def pytest_report_header(config: pytest.Config) -> str:
|
||||||
try:
|
try:
|
||||||
from PIL import features
|
from PIL import features
|
||||||
|
|
||||||
|
@ -13,7 +16,7 @@ def pytest_report_header(config):
|
||||||
return f"pytest_report_header failed: {e}"
|
return f"pytest_report_header failed: {e}"
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
def pytest_configure(config: pytest.Config) -> None:
|
||||||
config.addinivalue_line(
|
config.addinivalue_line(
|
||||||
"markers",
|
"markers",
|
||||||
"pil_noop_mark: A conditional mark where nothing special happens",
|
"pil_noop_mark: A conditional mark where nothing special happens",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
BIN
Tests/fonts/CBDTTestFont.ttf
Normal file
BIN
Tests/fonts/EBDTTestFont.ttf
Normal file
|
@ -2,7 +2,6 @@
|
||||||
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
|
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
|
||||||
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
|
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
|
||||||
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
|
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
|
||||||
NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji
|
|
||||||
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
|
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
|
||||||
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
|
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
|
||||||
|
@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3.
|
||||||
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
|
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
|
||||||
|
|
||||||
"Public domain font. Share and enjoy."
|
"Public domain font. Share and enjoy."
|
||||||
|
|
||||||
|
CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain.
|
||||||
|
|
145
Tests/helper.py
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Helper functions.
|
Helper functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -11,6 +12,7 @@ import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
import tempfile
|
import tempfile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import Any, Callable, Sequence
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
@ -19,42 +21,31 @@ from PIL import Image, ImageMath, features
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
uploader = None
|
||||||
HAS_UPLOADER = False
|
|
||||||
|
|
||||||
if os.environ.get("SHOW_ERRORS"):
|
if os.environ.get("SHOW_ERRORS"):
|
||||||
# local img.show for errors.
|
uploader = "show"
|
||||||
HAS_UPLOADER = True
|
elif "GITHUB_ACTIONS" in os.environ:
|
||||||
|
uploader = "github_actions"
|
||||||
|
|
||||||
class test_image_results:
|
|
||||||
@staticmethod
|
def upload(a: Image.Image, b: Image.Image) -> str | None:
|
||||||
def upload(a, b):
|
if uploader == "show":
|
||||||
|
# local img.show for errors.
|
||||||
a.show()
|
a.show()
|
||||||
b.show()
|
b.show()
|
||||||
|
elif uploader == "github_actions":
|
||||||
elif "GITHUB_ACTIONS" in os.environ:
|
|
||||||
HAS_UPLOADER = True
|
|
||||||
|
|
||||||
class test_image_results:
|
|
||||||
@staticmethod
|
|
||||||
def upload(a, b):
|
|
||||||
dir_errors = os.path.join(os.path.dirname(__file__), "errors")
|
dir_errors = os.path.join(os.path.dirname(__file__), "errors")
|
||||||
os.makedirs(dir_errors, exist_ok=True)
|
os.makedirs(dir_errors, exist_ok=True)
|
||||||
tmpdir = tempfile.mkdtemp(dir=dir_errors)
|
tmpdir = tempfile.mkdtemp(dir=dir_errors)
|
||||||
a.save(os.path.join(tmpdir, "a.png"))
|
a.save(os.path.join(tmpdir, "a.png"))
|
||||||
b.save(os.path.join(tmpdir, "b.png"))
|
b.save(os.path.join(tmpdir, "b.png"))
|
||||||
return tmpdir
|
return tmpdir
|
||||||
|
return None
|
||||||
else:
|
|
||||||
try:
|
|
||||||
import test_image_results
|
|
||||||
|
|
||||||
HAS_UPLOADER = True
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_comparable(a, b):
|
def convert_to_comparable(
|
||||||
|
a: Image.Image, b: Image.Image
|
||||||
|
) -> tuple[Image.Image, Image.Image]:
|
||||||
new_a, new_b = a, b
|
new_a, new_b = a, b
|
||||||
if a.mode == "P":
|
if a.mode == "P":
|
||||||
new_a = Image.new("L", a.size)
|
new_a = Image.new("L", a.size)
|
||||||
|
@ -67,14 +58,18 @@ def convert_to_comparable(a, b):
|
||||||
return new_a, new_b
|
return new_a, new_b
|
||||||
|
|
||||||
|
|
||||||
def assert_deep_equal(a, b, msg=None):
|
def assert_deep_equal(
|
||||||
|
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
|
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
|
||||||
except Exception:
|
except Exception:
|
||||||
assert a == b, msg
|
assert a == b, msg
|
||||||
|
|
||||||
|
|
||||||
def assert_image(im, mode, size, msg=None):
|
def assert_image(
|
||||||
|
im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None
|
||||||
|
) -> None:
|
||||||
if mode is not None:
|
if mode is not None:
|
||||||
assert im.mode == mode, (
|
assert im.mode == mode, (
|
||||||
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
|
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
|
||||||
|
@ -86,13 +81,13 @@ def assert_image(im, mode, size, msg=None):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def assert_image_equal(a, b, msg=None):
|
def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None:
|
||||||
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
|
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
|
||||||
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
|
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
|
||||||
if a.tobytes() != b.tobytes():
|
if a.tobytes() != b.tobytes():
|
||||||
if HAS_UPLOADER:
|
|
||||||
try:
|
try:
|
||||||
url = test_image_results.upload(a, b)
|
url = upload(a, b)
|
||||||
|
if url:
|
||||||
logger.error("URL for test images: %s", url)
|
logger.error("URL for test images: %s", url)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
@ -100,14 +95,18 @@ def assert_image_equal(a, b, msg=None):
|
||||||
pytest.fail(msg or "got different content")
|
pytest.fail(msg or "got different content")
|
||||||
|
|
||||||
|
|
||||||
def assert_image_equal_tofile(a, filename, msg=None, mode=None):
|
def assert_image_equal_tofile(
|
||||||
|
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
|
||||||
|
) -> None:
|
||||||
with Image.open(filename) as img:
|
with Image.open(filename) as img:
|
||||||
if mode:
|
if mode:
|
||||||
img = img.convert(mode)
|
img = img.convert(mode)
|
||||||
assert_image_equal(a, img, msg)
|
assert_image_equal(a, img, msg)
|
||||||
|
|
||||||
|
|
||||||
def assert_image_similar(a, b, epsilon, msg=None):
|
def assert_image_similar(
|
||||||
|
a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None
|
||||||
|
) -> None:
|
||||||
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
|
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
|
||||||
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
|
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
|
||||||
|
|
||||||
|
@ -125,55 +124,68 @@ def assert_image_similar(a, b, epsilon, msg=None):
|
||||||
+ f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
|
+ f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if HAS_UPLOADER:
|
|
||||||
try:
|
try:
|
||||||
url = test_image_results.upload(a, b)
|
url = upload(a, b)
|
||||||
|
if url:
|
||||||
logger.exception("URL for test images: %s", url)
|
logger.exception("URL for test images: %s", url)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None):
|
def assert_image_similar_tofile(
|
||||||
|
a: Image.Image,
|
||||||
|
filename: str,
|
||||||
|
epsilon: float,
|
||||||
|
msg: str | None = None,
|
||||||
|
mode: str | None = None,
|
||||||
|
) -> None:
|
||||||
with Image.open(filename) as img:
|
with Image.open(filename) as img:
|
||||||
if mode:
|
if mode:
|
||||||
img = img.convert(mode)
|
img = img.convert(mode)
|
||||||
assert_image_similar(a, img, epsilon, msg)
|
assert_image_similar(a, img, epsilon, msg)
|
||||||
|
|
||||||
|
|
||||||
def assert_all_same(items, msg=None):
|
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
|
||||||
assert items.count(items[0]) == len(items), msg
|
assert items.count(items[0]) == len(items), msg
|
||||||
|
|
||||||
|
|
||||||
def assert_not_all_same(items, msg=None):
|
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
|
||||||
assert items.count(items[0]) != len(items), msg
|
assert items.count(items[0]) != len(items), msg
|
||||||
|
|
||||||
|
|
||||||
def assert_tuple_approx_equal(actuals, targets, threshold, msg):
|
def assert_tuple_approx_equal(
|
||||||
|
actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str
|
||||||
|
) -> None:
|
||||||
"""Tests if actuals has values within threshold from targets"""
|
"""Tests if actuals has values within threshold from targets"""
|
||||||
value = True
|
|
||||||
for i, target in enumerate(targets):
|
for i, target in enumerate(targets):
|
||||||
value *= target - threshold <= actuals[i] <= target + threshold
|
if not (target - threshold <= actuals[i] <= target + threshold):
|
||||||
|
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
|
||||||
assert value, msg + ": " + repr(actuals) + " != " + repr(targets)
|
|
||||||
|
|
||||||
|
|
||||||
def skip_unless_feature(feature):
|
def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
|
||||||
reason = f"{feature} not available"
|
reason = f"{feature} not available"
|
||||||
return pytest.mark.skipif(not features.check(feature), reason=reason)
|
return pytest.mark.skipif(not features.check(feature), reason=reason)
|
||||||
|
|
||||||
|
|
||||||
def skip_unless_feature_version(feature, version_required, reason=None):
|
def skip_unless_feature_version(
|
||||||
|
feature: str, required: str, reason: str | None = None
|
||||||
|
) -> pytest.MarkDecorator:
|
||||||
if not features.check(feature):
|
if not features.check(feature):
|
||||||
return pytest.mark.skip(f"{feature} not available")
|
return pytest.mark.skip(f"{feature} not available")
|
||||||
if reason is None:
|
if reason is None:
|
||||||
reason = f"{feature} is older than {version_required}"
|
reason = f"{feature} is older than {required}"
|
||||||
version_required = parse_version(version_required)
|
version_required = parse_version(required)
|
||||||
version_available = parse_version(features.version(feature))
|
version_available = parse_version(features.version(feature))
|
||||||
return pytest.mark.skipif(version_available < version_required, reason=reason)
|
return pytest.mark.skipif(version_available < version_required, reason=reason)
|
||||||
|
|
||||||
|
|
||||||
def mark_if_feature_version(mark, feature, version_blacklist, reason=None):
|
def mark_if_feature_version(
|
||||||
|
mark: pytest.MarkDecorator,
|
||||||
|
feature: str,
|
||||||
|
version_blacklist: str,
|
||||||
|
reason: str | None = None,
|
||||||
|
) -> pytest.MarkDecorator:
|
||||||
if not features.check(feature):
|
if not features.check(feature):
|
||||||
return pytest.mark.pil_noop_mark()
|
return pytest.mark.pil_noop_mark()
|
||||||
if reason is None:
|
if reason is None:
|
||||||
|
@ -194,7 +206,7 @@ class PillowLeakTestCase:
|
||||||
iterations = 100 # count
|
iterations = 100 # count
|
||||||
mem_limit = 512 # k
|
mem_limit = 512 # k
|
||||||
|
|
||||||
def _get_mem_usage(self):
|
def _get_mem_usage(self) -> float:
|
||||||
"""
|
"""
|
||||||
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
|
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
|
||||||
between macOS and Linux rss reporting
|
between macOS and Linux rss reporting
|
||||||
|
@ -216,7 +228,7 @@ class PillowLeakTestCase:
|
||||||
# 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: Callable[[], None]) -> None:
|
||||||
start_mem = self._get_mem_usage()
|
start_mem = self._get_mem_usage()
|
||||||
for cycle in range(self.iterations):
|
for cycle in range(self.iterations):
|
||||||
core()
|
core()
|
||||||
|
@ -228,17 +240,17 @@ class PillowLeakTestCase:
|
||||||
# helpers
|
# helpers
|
||||||
|
|
||||||
|
|
||||||
def fromstring(data):
|
def fromstring(data: bytes) -> Image.Image:
|
||||||
return Image.open(BytesIO(data))
|
return Image.open(BytesIO(data))
|
||||||
|
|
||||||
|
|
||||||
def tostring(im, string_format, **options):
|
def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im.save(out, string_format, **options)
|
im.save(out, string_format, **options)
|
||||||
return out.getvalue()
|
return out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def hopper(mode=None, cache={}):
|
def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
|
||||||
if mode is None:
|
if mode is None:
|
||||||
# Always return fresh not-yet-loaded version of image.
|
# Always return fresh not-yet-loaded version of image.
|
||||||
# Operations on not-yet-loaded images is separate class of errors
|
# Operations on not-yet-loaded images is separate class of errors
|
||||||
|
@ -259,29 +271,31 @@ def hopper(mode=None, cache={}):
|
||||||
return im.copy()
|
return im.copy()
|
||||||
|
|
||||||
|
|
||||||
def djpeg_available():
|
def djpeg_available() -> bool:
|
||||||
if shutil.which("djpeg"):
|
if shutil.which("djpeg"):
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(["djpeg", "-version"])
|
subprocess.check_call(["djpeg", "-version"])
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError: # pragma: no cover
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def cjpeg_available():
|
def cjpeg_available() -> bool:
|
||||||
if shutil.which("cjpeg"):
|
if shutil.which("cjpeg"):
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(["cjpeg", "-version"])
|
subprocess.check_call(["cjpeg", "-version"])
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError: # pragma: no cover
|
except subprocess.CalledProcessError: # pragma: no cover
|
||||||
return False
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def netpbm_available():
|
def netpbm_available() -> bool:
|
||||||
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
|
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
|
||||||
|
|
||||||
|
|
||||||
def magick_command():
|
def magick_command() -> list[str] | None:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
magickhome = os.environ.get("MAGICK_HOME")
|
magickhome = os.environ.get("MAGICK_HOME")
|
||||||
if magickhome:
|
if magickhome:
|
||||||
|
@ -298,47 +312,48 @@ def magick_command():
|
||||||
return imagemagick
|
return imagemagick
|
||||||
if graphicsmagick and shutil.which(graphicsmagick[0]):
|
if graphicsmagick and shutil.which(graphicsmagick[0]):
|
||||||
return graphicsmagick
|
return graphicsmagick
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def on_appveyor():
|
def on_appveyor() -> bool:
|
||||||
return "APPVEYOR" in os.environ
|
return "APPVEYOR" in os.environ
|
||||||
|
|
||||||
|
|
||||||
def on_github_actions():
|
def on_github_actions() -> bool:
|
||||||
return "GITHUB_ACTIONS" in os.environ
|
return "GITHUB_ACTIONS" in os.environ
|
||||||
|
|
||||||
|
|
||||||
def on_ci():
|
def on_ci() -> bool:
|
||||||
# GitHub Actions and AppVeyor have "CI"
|
# GitHub Actions and AppVeyor have "CI"
|
||||||
return "CI" in os.environ
|
return "CI" in os.environ
|
||||||
|
|
||||||
|
|
||||||
def is_big_endian():
|
def is_big_endian() -> bool:
|
||||||
return sys.byteorder == "big"
|
return sys.byteorder == "big"
|
||||||
|
|
||||||
|
|
||||||
def is_ppc64le():
|
def is_ppc64le() -> bool:
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
return platform.machine() == "ppc64le"
|
return platform.machine() == "ppc64le"
|
||||||
|
|
||||||
|
|
||||||
def is_win32():
|
def is_win32() -> bool:
|
||||||
return sys.platform.startswith("win32")
|
return sys.platform.startswith("win32")
|
||||||
|
|
||||||
|
|
||||||
def is_pypy():
|
def is_pypy() -> bool:
|
||||||
return hasattr(sys, "pypy_translation_info")
|
return hasattr(sys, "pypy_translation_info")
|
||||||
|
|
||||||
|
|
||||||
def is_mingw():
|
def is_mingw() -> bool:
|
||||||
return sysconfig.get_platform() == "mingw"
|
return sysconfig.get_platform() == "mingw"
|
||||||
|
|
||||||
|
|
||||||
class CachedProperty:
|
class CachedProperty:
|
||||||
def __init__(self, func):
|
def __init__(self, func: Callable[[Any], None]) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def __get__(self, instance, cls=None):
|
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
|
||||||
result = instance.__dict__[self.func.__name__] = self.func(instance)
|
result = instance.__dict__[self.func.__name__] = self.func(instance)
|
||||||
return result
|
return result
|
||||||
|
|
BIN
Tests/images/2422.flc
Normal file
BIN
Tests/images/apng/different_durations.png
Normal file
After Width: | Height: | Size: 233 B |
BIN
Tests/images/bgr15.dds
Normal file
BIN
Tests/images/bgr15.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
Tests/images/bitmap_font_blend.png
Normal file
After Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
Tests/images/cbdt.png
Normal file
After Width: | Height: | Size: 348 B |
BIN
Tests/images/cbdt_mask.png
Normal file
After Width: | Height: | Size: 367 B |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
|
@ -1,5 +1,3 @@
|
||||||
#!/usr/bin/gnuplot
|
|
||||||
|
|
||||||
#This is the script that was used to create our sample EPS files
|
#This is the script that was used to create our sample EPS files
|
||||||
#We used the following version of the gnuplot program
|
#We used the following version of the gnuplot program
|
||||||
#G N U P L O T
|
#G N U P L O T
|
||||||
|
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
Tests/images/hopper.pfm
Normal file
BIN
Tests/images/hopper_be.pfm
Normal file
BIN
Tests/images/multiple_exif.jpg
Normal file
After Width: | Height: | Size: 364 B |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
@ -13,7 +13,6 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
import atheris
|
import atheris
|
||||||
|
@ -24,7 +23,7 @@ with atheris.instrument_imports():
|
||||||
import fuzzers
|
import fuzzers
|
||||||
|
|
||||||
|
|
||||||
def TestOneInput(data):
|
def TestOneInput(data: bytes) -> None:
|
||||||
try:
|
try:
|
||||||
fuzzers.fuzz_font(data)
|
fuzzers.fuzz_font(data)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -33,7 +32,7 @@ def TestOneInput(data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
fuzzers.enable_decompressionbomb_error()
|
fuzzers.enable_decompressionbomb_error()
|
||||||
atheris.Setup(sys.argv, TestOneInput)
|
atheris.Setup(sys.argv, TestOneInput)
|
||||||
atheris.Fuzz()
|
atheris.Fuzz()
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#!/usr/bin/python3
|
|
||||||
|
|
||||||
# Copyright 2020 Google LLC
|
# Copyright 2020 Google LLC
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -13,7 +11,6 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
import atheris
|
import atheris
|
||||||
|
@ -24,7 +21,7 @@ with atheris.instrument_imports():
|
||||||
import fuzzers
|
import fuzzers
|
||||||
|
|
||||||
|
|
||||||
def TestOneInput(data):
|
def TestOneInput(data: bytes) -> None:
|
||||||
try:
|
try:
|
||||||
fuzzers.fuzz_image(data)
|
fuzzers.fuzz_image(data)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -33,7 +30,7 @@ def TestOneInput(data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
fuzzers.enable_decompressionbomb_error()
|
fuzzers.enable_decompressionbomb_error()
|
||||||
atheris.Setup(sys.argv, TestOneInput)
|
atheris.Setup(sys.argv, TestOneInput)
|
||||||
atheris.Fuzz()
|
atheris.Fuzz()
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont
|
from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont
|
||||||
|
|
||||||
|
|
||||||
def enable_decompressionbomb_error():
|
def enable_decompressionbomb_error() -> None:
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
warnings.simplefilter("error", Image.DecompressionBombWarning)
|
warnings.simplefilter("error", Image.DecompressionBombWarning)
|
||||||
|
|
||||||
|
|
||||||
def disable_decompressionbomb_error():
|
def disable_decompressionbomb_error() -> None:
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
warnings.resetwarnings()
|
warnings.resetwarnings()
|
||||||
|
|
||||||
|
|
||||||
def fuzz_image(data):
|
def fuzz_image(data: bytes) -> None:
|
||||||
# This will fail on some images in the corpus, as we have many
|
# This will fail on some images in the corpus, as we have many
|
||||||
# invalid images in the test suite.
|
# invalid images in the test suite.
|
||||||
with Image.open(io.BytesIO(data)) as im:
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
@ -25,7 +26,7 @@ def fuzz_image(data):
|
||||||
im.save(io.BytesIO(), "BMP")
|
im.save(io.BytesIO(), "BMP")
|
||||||
|
|
||||||
|
|
||||||
def fuzz_font(data):
|
def fuzz_font(data: bytes) -> None:
|
||||||
wrapper = io.BytesIO(data)
|
wrapper = io.BytesIO(data)
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(wrapper)
|
font = ImageFont.truetype(wrapper)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ if features.check("libjpeg_turbo"):
|
||||||
"path",
|
"path",
|
||||||
subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"),
|
subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"),
|
||||||
)
|
)
|
||||||
def test_fuzz_images(path):
|
def test_fuzz_images(path: str) -> None:
|
||||||
fuzzers.enable_decompressionbomb_error()
|
fuzzers.enable_decompressionbomb_error()
|
||||||
try:
|
try:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
|
@ -54,7 +55,7 @@ def test_fuzz_images(path):
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
|
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
|
||||||
)
|
)
|
||||||
def test_fuzz_fonts(path):
|
def test_fuzz_fonts(path: str) -> None:
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
# Make sure we have the binary extension
|
# Make sure we have the binary extension
|
||||||
Image.core.new("L", (100, 100))
|
Image.core.new("L", (100, 100))
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PIL import _binary
|
from PIL import _binary
|
||||||
|
|
||||||
|
|
||||||
def test_standard():
|
def test_standard() -> None:
|
||||||
assert _binary.i8(b"*") == 42
|
assert _binary.i8(b"*") == 42
|
||||||
assert _binary.o8(42) == b"*"
|
assert _binary.o8(42) == b"*"
|
||||||
|
|
||||||
|
|
||||||
def test_little_endian():
|
def test_little_endian() -> None:
|
||||||
assert _binary.i16le(b"\xff\xff\x00\x00") == 65535
|
assert _binary.i16le(b"\xff\xff\x00\x00") == 65535
|
||||||
assert _binary.i32le(b"\xff\xff\x00\x00") == 65535
|
assert _binary.i32le(b"\xff\xff\x00\x00") == 65535
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ def test_little_endian():
|
||||||
assert _binary.o32le(65535) == b"\xff\xff\x00\x00"
|
assert _binary.o32le(65535) == b"\xff\xff\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
def test_big_endian():
|
def test_big_endian() -> None:
|
||||||
assert _binary.i16be(b"\x00\x00\xff\xff") == 0
|
assert _binary.i16be(b"\x00\x00\xff\xff") == 0
|
||||||
assert _binary.i32be(b"\x00\x00\xff\xff") == 65535
|
assert _binary.i32be(b"\x00\x00\xff\xff") == 65535
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -9,13 +10,13 @@ from .helper import assert_image_similar
|
||||||
base = os.path.join("Tests", "images", "bmp")
|
base = os.path.join("Tests", "images", "bmp")
|
||||||
|
|
||||||
|
|
||||||
def get_files(d, ext=".bmp"):
|
def get_files(d: str, ext: str = ".bmp") -> list[str]:
|
||||||
return [
|
return [
|
||||||
os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f
|
os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_bad():
|
def test_bad() -> None:
|
||||||
"""These shouldn't crash/dos, but they shouldn't return anything
|
"""These shouldn't crash/dos, but they shouldn't return anything
|
||||||
either"""
|
either"""
|
||||||
for f in get_files("b"):
|
for f in get_files("b"):
|
||||||
|
@ -28,7 +29,7 @@ def test_bad():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_questionable():
|
def test_questionable() -> None:
|
||||||
"""These shouldn't crash/dos, but it's not well defined that these
|
"""These shouldn't crash/dos, but it's not well defined that these
|
||||||
are in spec"""
|
are in spec"""
|
||||||
supported = [
|
supported = [
|
||||||
|
@ -55,7 +56,7 @@ def test_questionable():
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def test_good():
|
def test_good() -> None:
|
||||||
"""These should all work. There's a set of target files in the
|
"""These should all work. There's a set of target files in the
|
||||||
html directory that we can compare against."""
|
html directory that we can compare against."""
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ def test_good():
|
||||||
"rgb32bf.bmp": "rgb24.png",
|
"rgb32bf.bmp": "rgb24.png",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_compare(f):
|
def get_compare(f: str) -> str:
|
||||||
name = os.path.split(f)[1]
|
name = os.path.split(f)[1]
|
||||||
if name in file_map:
|
if name in file_map:
|
||||||
return os.path.join(base, "html", file_map[name])
|
return os.path.join(base, "html", file_map[name])
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFilter
|
from PIL import Image, ImageFilter
|
||||||
|
@ -15,18 +16,18 @@ sample.putdata(sum([
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
def test_imageops_box_blur():
|
def test_imageops_box_blur() -> None:
|
||||||
i = sample.filter(ImageFilter.BoxBlur(1))
|
i = sample.filter(ImageFilter.BoxBlur(1))
|
||||||
assert i.mode == sample.mode
|
assert i.mode == sample.mode
|
||||||
assert i.size == sample.size
|
assert i.size == sample.size
|
||||||
assert isinstance(i, Image.Image)
|
assert isinstance(i, Image.Image)
|
||||||
|
|
||||||
|
|
||||||
def box_blur(image, radius=1, n=1):
|
def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image:
|
||||||
return image._new(image.im.box_blur((radius, radius), n))
|
return image._new(image.im.box_blur((radius, radius), n))
|
||||||
|
|
||||||
|
|
||||||
def assert_image(im, data, delta=0):
|
def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None:
|
||||||
it = iter(im.getdata())
|
it = iter(im.getdata())
|
||||||
for data_row in data:
|
for data_row in data:
|
||||||
im_row = [next(it) for _ in range(im.size[0])]
|
im_row = [next(it) for _ in range(im.size[0])]
|
||||||
|
@ -36,7 +37,13 @@ def assert_image(im, data, delta=0):
|
||||||
next(it)
|
next(it)
|
||||||
|
|
||||||
|
|
||||||
def assert_blur(im, radius, data, passes=1, delta=0):
|
def assert_blur(
|
||||||
|
im: Image.Image,
|
||||||
|
radius: float,
|
||||||
|
data: list[list[int]],
|
||||||
|
passes: int = 1,
|
||||||
|
delta: int = 0,
|
||||||
|
) -> None:
|
||||||
# check grayscale image
|
# check grayscale image
|
||||||
assert_image(box_blur(im, radius, passes), data, delta)
|
assert_image(box_blur(im, radius, passes), data, delta)
|
||||||
rgba = Image.merge("RGBA", (im, im, im, im))
|
rgba = Image.merge("RGBA", (im, im, im, im))
|
||||||
|
@ -44,7 +51,7 @@ def assert_blur(im, radius, data, passes=1, delta=0):
|
||||||
assert_image(band, data, delta)
|
assert_image(band, data, delta)
|
||||||
|
|
||||||
|
|
||||||
def test_color_modes():
|
def test_color_modes() -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
box_blur(sample.convert("1"))
|
box_blur(sample.convert("1"))
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -64,7 +71,7 @@ def test_color_modes():
|
||||||
box_blur(sample.convert("YCbCr"))
|
box_blur(sample.convert("YCbCr"))
|
||||||
|
|
||||||
|
|
||||||
def test_radius_0():
|
def test_radius_0() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
0,
|
0,
|
||||||
|
@ -80,7 +87,7 @@ def test_radius_0():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_0_02():
|
def test_radius_0_02() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
0.02,
|
0.02,
|
||||||
|
@ -97,7 +104,7 @@ def test_radius_0_02():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_0_05():
|
def test_radius_0_05() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
0.05,
|
0.05,
|
||||||
|
@ -114,7 +121,7 @@ def test_radius_0_05():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_0_1():
|
def test_radius_0_1() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
0.1,
|
0.1,
|
||||||
|
@ -131,7 +138,7 @@ def test_radius_0_1():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_0_5():
|
def test_radius_0_5() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
0.5,
|
0.5,
|
||||||
|
@ -148,7 +155,7 @@ def test_radius_0_5():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_1():
|
def test_radius_1() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
1,
|
1,
|
||||||
|
@ -165,7 +172,7 @@ def test_radius_1():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_1_5():
|
def test_radius_1_5() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
1.5,
|
1.5,
|
||||||
|
@ -182,7 +189,7 @@ def test_radius_1_5():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_bigger_then_half():
|
def test_radius_bigger_then_half() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
3,
|
3,
|
||||||
|
@ -199,7 +206,7 @@ def test_radius_bigger_then_half():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_radius_bigger_then_width():
|
def test_radius_bigger_then_width() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
10,
|
10,
|
||||||
|
@ -214,7 +221,7 @@ def test_radius_bigger_then_width():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_extreme_large_radius():
|
def test_extreme_large_radius() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
600,
|
600,
|
||||||
|
@ -229,7 +236,7 @@ def test_extreme_large_radius():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_two_passes():
|
def test_two_passes() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
1,
|
1,
|
||||||
|
@ -247,7 +254,7 @@ def test_two_passes():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_three_passes():
|
def test_three_passes() -> None:
|
||||||
assert_blur(
|
assert_blur(
|
||||||
sample,
|
sample,
|
||||||
1,
|
1,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from array import array
|
from array import array
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -7,6 +9,7 @@ from PIL import Image, ImageFilter
|
||||||
|
|
||||||
from .helper import assert_image_equal
|
from .helper import assert_image_equal
|
||||||
|
|
||||||
|
numpy: ModuleType | None
|
||||||
try:
|
try:
|
||||||
import numpy
|
import numpy
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -14,7 +17,9 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
class TestColorLut3DCoreAPI:
|
class TestColorLut3DCoreAPI:
|
||||||
def generate_identity_table(self, channels, size):
|
def generate_identity_table(
|
||||||
|
self, channels: int, size: int | tuple[int, int, int]
|
||||||
|
) -> tuple[int, int, int, int, list[float]]:
|
||||||
if isinstance(size, tuple):
|
if isinstance(size, tuple):
|
||||||
size_1d, size_2d, size_3d = size
|
size_1d, size_2d, size_3d = size
|
||||||
else:
|
else:
|
||||||
|
@ -40,7 +45,7 @@ class TestColorLut3DCoreAPI:
|
||||||
[item for sublist in table for item in sublist],
|
[item for sublist in table for item in sublist],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_wrong_args(self):
|
def test_wrong_args(self) -> None:
|
||||||
im = Image.new("RGB", (10, 10), 0)
|
im = Image.new("RGB", (10, 10), 0)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="filter"):
|
with pytest.raises(ValueError, match="filter"):
|
||||||
|
@ -100,7 +105,7 @@ class TestColorLut3DCoreAPI:
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
|
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
|
||||||
|
|
||||||
def test_correct_args(self):
|
def test_correct_args(self) -> None:
|
||||||
im = Image.new("RGB", (10, 10), 0)
|
im = Image.new("RGB", (10, 10), 0)
|
||||||
|
|
||||||
im.im.color_lut_3d(
|
im.im.color_lut_3d(
|
||||||
|
@ -135,7 +140,7 @@ class TestColorLut3DCoreAPI:
|
||||||
*self.generate_identity_table(3, (3, 3, 65)),
|
*self.generate_identity_table(3, (3, 3, 65)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_wrong_mode(self):
|
def test_wrong_mode(self) -> None:
|
||||||
with pytest.raises(ValueError, match="wrong mode"):
|
with pytest.raises(ValueError, match="wrong mode"):
|
||||||
im = Image.new("L", (10, 10), 0)
|
im = Image.new("L", (10, 10), 0)
|
||||||
im.im.color_lut_3d(
|
im.im.color_lut_3d(
|
||||||
|
@ -166,7 +171,7 @@ class TestColorLut3DCoreAPI:
|
||||||
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_correct_mode(self):
|
def test_correct_mode(self) -> None:
|
||||||
im = Image.new("RGBA", (10, 10), 0)
|
im = Image.new("RGBA", (10, 10), 0)
|
||||||
im.im.color_lut_3d(
|
im.im.color_lut_3d(
|
||||||
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||||
|
@ -187,7 +192,7 @@ class TestColorLut3DCoreAPI:
|
||||||
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_identities(self):
|
def test_identities(self) -> None:
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
im = Image.merge(
|
im = Image.merge(
|
||||||
"RGB",
|
"RGB",
|
||||||
|
@ -223,7 +228,7 @@ class TestColorLut3DCoreAPI:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_identities_4_channels(self):
|
def test_identities_4_channels(self) -> None:
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
im = Image.merge(
|
im = Image.merge(
|
||||||
"RGB",
|
"RGB",
|
||||||
|
@ -246,7 +251,7 @@ class TestColorLut3DCoreAPI:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_copy_alpha_channel(self):
|
def test_copy_alpha_channel(self) -> None:
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
im = Image.merge(
|
im = Image.merge(
|
||||||
"RGBA",
|
"RGBA",
|
||||||
|
@ -269,7 +274,7 @@ class TestColorLut3DCoreAPI:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_channels_order(self):
|
def test_channels_order(self) -> None:
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
im = Image.merge(
|
im = Image.merge(
|
||||||
"RGB",
|
"RGB",
|
||||||
|
@ -294,7 +299,7 @@ class TestColorLut3DCoreAPI:
|
||||||
])))
|
])))
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
def test_overflow(self):
|
def test_overflow(self) -> None:
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
im = Image.merge(
|
im = Image.merge(
|
||||||
"RGB",
|
"RGB",
|
||||||
|
@ -347,7 +352,7 @@ class TestColorLut3DCoreAPI:
|
||||||
|
|
||||||
|
|
||||||
class TestColorLut3DFilter:
|
class TestColorLut3DFilter:
|
||||||
def test_wrong_args(self):
|
def test_wrong_args(self) -> None:
|
||||||
with pytest.raises(ValueError, match="should be either an integer"):
|
with pytest.raises(ValueError, match="should be either an integer"):
|
||||||
ImageFilter.Color3DLUT("small", [1])
|
ImageFilter.Color3DLUT("small", [1])
|
||||||
|
|
||||||
|
@ -375,7 +380,7 @@ class TestColorLut3DFilter:
|
||||||
with pytest.raises(ValueError, match="Only 3 or 4 output"):
|
with pytest.raises(ValueError, match="Only 3 or 4 output"):
|
||||||
ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2)
|
ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2)
|
||||||
|
|
||||||
def test_convert_table(self):
|
def test_convert_table(self) -> None:
|
||||||
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
|
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
|
||||||
assert tuple(lut.size) == (2, 2, 2)
|
assert tuple(lut.size) == (2, 2, 2)
|
||||||
assert lut.name == "Color 3D LUT"
|
assert lut.name == "Color 3D LUT"
|
||||||
|
@ -393,7 +398,8 @@ class TestColorLut3DFilter:
|
||||||
assert lut.table == list(range(4)) * 8
|
assert lut.table == list(range(4)) * 8
|
||||||
|
|
||||||
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
||||||
def test_numpy_sources(self):
|
def test_numpy_sources(self) -> None:
|
||||||
|
assert numpy is not None
|
||||||
table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
|
table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
|
||||||
with pytest.raises(ValueError, match="should have either channels"):
|
with pytest.raises(ValueError, match="should have either channels"):
|
||||||
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
||||||
|
@ -426,7 +432,8 @@ class TestColorLut3DFilter:
|
||||||
assert lut.table[0] == 33
|
assert lut.table[0] == 33
|
||||||
|
|
||||||
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
|
||||||
def test_numpy_formats(self):
|
def test_numpy_formats(self) -> None:
|
||||||
|
assert numpy is not None
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
im = Image.merge(
|
im = Image.merge(
|
||||||
"RGB",
|
"RGB",
|
||||||
|
@ -465,7 +472,7 @@ class TestColorLut3DFilter:
|
||||||
lut.table = numpy.array(lut.table, dtype=numpy.int8)
|
lut.table = numpy.array(lut.table, dtype=numpy.int8)
|
||||||
im.filter(lut)
|
im.filter(lut)
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self) -> None:
|
||||||
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
|
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
|
||||||
assert repr(lut) == "<Color3DLUT from list size=2x2x2 channels=3>"
|
assert repr(lut) == "<Color3DLUT from list size=2x2x2 channels=3>"
|
||||||
|
|
||||||
|
@ -483,7 +490,7 @@ class TestColorLut3DFilter:
|
||||||
|
|
||||||
|
|
||||||
class TestGenerateColorLut3D:
|
class TestGenerateColorLut3D:
|
||||||
def test_wrong_channels_count(self):
|
def test_wrong_channels_count(self) -> None:
|
||||||
with pytest.raises(ValueError, match="3 or 4 output channels"):
|
with pytest.raises(ValueError, match="3 or 4 output channels"):
|
||||||
ImageFilter.Color3DLUT.generate(
|
ImageFilter.Color3DLUT.generate(
|
||||||
5, channels=2, callback=lambda r, g, b: (r, g, b)
|
5, channels=2, callback=lambda r, g, b: (r, g, b)
|
||||||
|
@ -497,7 +504,7 @@ class TestGenerateColorLut3D:
|
||||||
5, channels=4, callback=lambda r, g, b: (r, g, b)
|
5, channels=4, callback=lambda r, g, b: (r, g, b)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_3_channels(self):
|
def test_3_channels(self) -> None:
|
||||||
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
|
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
|
||||||
assert tuple(lut.size) == (5, 5, 5)
|
assert tuple(lut.size) == (5, 5, 5)
|
||||||
assert lut.name == "Color 3D LUT"
|
assert lut.name == "Color 3D LUT"
|
||||||
|
@ -507,7 +514,7 @@ class TestGenerateColorLut3D:
|
||||||
1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0]
|
1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
def test_4_channels(self):
|
def test_4_channels(self) -> None:
|
||||||
lut = ImageFilter.Color3DLUT.generate(
|
lut = ImageFilter.Color3DLUT.generate(
|
||||||
5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2)
|
5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2)
|
||||||
)
|
)
|
||||||
|
@ -520,7 +527,7 @@ class TestGenerateColorLut3D:
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
def test_apply(self):
|
def test_apply(self) -> None:
|
||||||
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
|
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
|
||||||
|
|
||||||
g = Image.linear_gradient("L")
|
g = Image.linear_gradient("L")
|
||||||
|
@ -536,7 +543,7 @@ class TestGenerateColorLut3D:
|
||||||
|
|
||||||
|
|
||||||
class TestTransformColorLut3D:
|
class TestTransformColorLut3D:
|
||||||
def test_wrong_args(self):
|
def test_wrong_args(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
|
source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Only 3 or 4 output"):
|
with pytest.raises(ValueError, match="Only 3 or 4 output"):
|
||||||
|
@ -551,7 +558,7 @@ class TestTransformColorLut3D:
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
source.transform(lambda r, g, b, a: (r, g, b))
|
source.transform(lambda r, g, b, a: (r, g, b))
|
||||||
|
|
||||||
def test_target_mode(self):
|
def test_target_mode(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate(
|
source = ImageFilter.Color3DLUT.generate(
|
||||||
2, lambda r, g, b: (r, g, b), target_mode="HSV"
|
2, lambda r, g, b: (r, g, b), target_mode="HSV"
|
||||||
)
|
)
|
||||||
|
@ -562,7 +569,7 @@ class TestTransformColorLut3D:
|
||||||
lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB")
|
lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB")
|
||||||
assert lut.mode == "RGB"
|
assert lut.mode == "RGB"
|
||||||
|
|
||||||
def test_3_to_3_channels(self):
|
def test_3_to_3_channels(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b))
|
source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b))
|
||||||
lut = source.transform(lambda r, g, b: (r * r, g * g, b * b))
|
lut = source.transform(lambda r, g, b: (r * r, g * g, b * b))
|
||||||
assert tuple(lut.size) == tuple(source.size)
|
assert tuple(lut.size) == tuple(source.size)
|
||||||
|
@ -570,7 +577,7 @@ class TestTransformColorLut3D:
|
||||||
assert lut.table != source.table
|
assert lut.table != source.table
|
||||||
assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
|
assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
|
||||||
|
|
||||||
def test_3_to_4_channels(self):
|
def test_3_to_4_channels(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
|
source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
|
||||||
lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4)
|
lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4)
|
||||||
assert tuple(lut.size) == tuple(source.size)
|
assert tuple(lut.size) == tuple(source.size)
|
||||||
|
@ -582,7 +589,7 @@ class TestTransformColorLut3D:
|
||||||
0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]
|
0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
def test_4_to_3_channels(self):
|
def test_4_to_3_channels(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate(
|
source = ImageFilter.Color3DLUT.generate(
|
||||||
(3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4
|
(3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4
|
||||||
)
|
)
|
||||||
|
@ -598,7 +605,7 @@ class TestTransformColorLut3D:
|
||||||
1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]
|
1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
def test_4_to_4_channels(self):
|
def test_4_to_4_channels(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate(
|
source = ImageFilter.Color3DLUT.generate(
|
||||||
(6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4
|
(6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4
|
||||||
)
|
)
|
||||||
|
@ -612,7 +619,7 @@ class TestTransformColorLut3D:
|
||||||
0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]
|
0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
def test_with_normals_3_channels(self):
|
def test_with_normals_3_channels(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate(
|
source = ImageFilter.Color3DLUT.generate(
|
||||||
(6, 5, 4), lambda r, g, b: (r * r, g * g, b * b)
|
(6, 5, 4), lambda r, g, b: (r * r, g * g, b * b)
|
||||||
)
|
)
|
||||||
|
@ -628,7 +635,7 @@ class TestTransformColorLut3D:
|
||||||
0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]
|
0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
def test_with_normals_4_channels(self):
|
def test_with_normals_4_channels(self) -> None:
|
||||||
source = ImageFilter.Color3DLUT.generate(
|
source = ImageFilter.Color3DLUT.generate(
|
||||||
(3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4
|
(3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -8,7 +9,7 @@ from PIL import Image
|
||||||
from .helper import is_pypy
|
from .helper import is_pypy
|
||||||
|
|
||||||
|
|
||||||
def test_get_stats():
|
def test_get_stats() -> None:
|
||||||
# Create at least one image
|
# Create at least one image
|
||||||
Image.new("RGB", (10, 10))
|
Image.new("RGB", (10, 10))
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ def test_get_stats():
|
||||||
assert "blocks_cached" in stats
|
assert "blocks_cached" in stats
|
||||||
|
|
||||||
|
|
||||||
def test_reset_stats():
|
def test_reset_stats() -> None:
|
||||||
Image.core.reset_stats()
|
Image.core.reset_stats()
|
||||||
|
|
||||||
stats = Image.core.get_stats()
|
stats = Image.core.get_stats()
|
||||||
|
@ -34,19 +35,19 @@ def test_reset_stats():
|
||||||
|
|
||||||
|
|
||||||
class TestCoreMemory:
|
class TestCoreMemory:
|
||||||
def teardown_method(self):
|
def teardown_method(self) -> None:
|
||||||
# Restore default values
|
# Restore default values
|
||||||
Image.core.set_alignment(1)
|
Image.core.set_alignment(1)
|
||||||
Image.core.set_block_size(1024 * 1024)
|
Image.core.set_block_size(1024 * 1024)
|
||||||
Image.core.set_blocks_max(0)
|
Image.core.set_blocks_max(0)
|
||||||
Image.core.clear_cache()
|
Image.core.clear_cache()
|
||||||
|
|
||||||
def test_get_alignment(self):
|
def test_get_alignment(self) -> None:
|
||||||
alignment = Image.core.get_alignment()
|
alignment = Image.core.get_alignment()
|
||||||
|
|
||||||
assert alignment > 0
|
assert alignment > 0
|
||||||
|
|
||||||
def test_set_alignment(self):
|
def test_set_alignment(self) -> None:
|
||||||
for i in [1, 2, 4, 8, 16, 32]:
|
for i in [1, 2, 4, 8, 16, 32]:
|
||||||
Image.core.set_alignment(i)
|
Image.core.set_alignment(i)
|
||||||
alignment = Image.core.get_alignment()
|
alignment = Image.core.get_alignment()
|
||||||
|
@ -62,12 +63,12 @@ class TestCoreMemory:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
Image.core.set_alignment(3)
|
Image.core.set_alignment(3)
|
||||||
|
|
||||||
def test_get_block_size(self):
|
def test_get_block_size(self) -> None:
|
||||||
block_size = Image.core.get_block_size()
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
assert block_size >= 4096
|
assert block_size >= 4096
|
||||||
|
|
||||||
def test_set_block_size(self):
|
def test_set_block_size(self) -> None:
|
||||||
for i in [4096, 2 * 4096, 3 * 4096]:
|
for i in [4096, 2 * 4096, 3 * 4096]:
|
||||||
Image.core.set_block_size(i)
|
Image.core.set_block_size(i)
|
||||||
block_size = Image.core.get_block_size()
|
block_size = Image.core.get_block_size()
|
||||||
|
@ -83,7 +84,7 @@ class TestCoreMemory:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
Image.core.set_block_size(4000)
|
Image.core.set_block_size(4000)
|
||||||
|
|
||||||
def test_set_block_size_stats(self):
|
def test_set_block_size_stats(self) -> None:
|
||||||
Image.core.reset_stats()
|
Image.core.reset_stats()
|
||||||
Image.core.set_blocks_max(0)
|
Image.core.set_blocks_max(0)
|
||||||
Image.core.set_block_size(4096)
|
Image.core.set_block_size(4096)
|
||||||
|
@ -95,12 +96,12 @@ class TestCoreMemory:
|
||||||
if not is_pypy():
|
if not is_pypy():
|
||||||
assert stats["freed_blocks"] >= 64
|
assert stats["freed_blocks"] >= 64
|
||||||
|
|
||||||
def test_get_blocks_max(self):
|
def test_get_blocks_max(self) -> None:
|
||||||
blocks_max = Image.core.get_blocks_max()
|
blocks_max = Image.core.get_blocks_max()
|
||||||
|
|
||||||
assert blocks_max >= 0
|
assert blocks_max >= 0
|
||||||
|
|
||||||
def test_set_blocks_max(self):
|
def test_set_blocks_max(self) -> None:
|
||||||
for i in [0, 1, 10]:
|
for i in [0, 1, 10]:
|
||||||
Image.core.set_blocks_max(i)
|
Image.core.set_blocks_max(i)
|
||||||
blocks_max = Image.core.get_blocks_max()
|
blocks_max = Image.core.get_blocks_max()
|
||||||
|
@ -116,7 +117,7 @@ class TestCoreMemory:
|
||||||
Image.core.set_blocks_max(2**29)
|
Image.core.set_blocks_max(2**29)
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
|
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
|
||||||
def test_set_blocks_max_stats(self):
|
def test_set_blocks_max_stats(self) -> None:
|
||||||
Image.core.reset_stats()
|
Image.core.reset_stats()
|
||||||
Image.core.set_blocks_max(128)
|
Image.core.set_blocks_max(128)
|
||||||
Image.core.set_block_size(4096)
|
Image.core.set_block_size(4096)
|
||||||
|
@ -131,7 +132,7 @@ class TestCoreMemory:
|
||||||
assert stats["blocks_cached"] == 64
|
assert stats["blocks_cached"] == 64
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
|
@pytest.mark.skipif(is_pypy(), reason="Images not collected")
|
||||||
def test_clear_cache_stats(self):
|
def test_clear_cache_stats(self) -> None:
|
||||||
Image.core.reset_stats()
|
Image.core.reset_stats()
|
||||||
Image.core.clear_cache()
|
Image.core.clear_cache()
|
||||||
Image.core.set_blocks_max(128)
|
Image.core.set_blocks_max(128)
|
||||||
|
@ -148,7 +149,7 @@ class TestCoreMemory:
|
||||||
assert stats["freed_blocks"] >= 48
|
assert stats["freed_blocks"] >= 48
|
||||||
assert stats["blocks_cached"] == 16
|
assert stats["blocks_cached"] == 16
|
||||||
|
|
||||||
def test_large_images(self):
|
def test_large_images(self) -> None:
|
||||||
Image.core.reset_stats()
|
Image.core.reset_stats()
|
||||||
Image.core.set_blocks_max(0)
|
Image.core.set_blocks_max(0)
|
||||||
Image.core.set_block_size(4096)
|
Image.core.set_block_size(4096)
|
||||||
|
@ -165,14 +166,14 @@ class TestCoreMemory:
|
||||||
|
|
||||||
|
|
||||||
class TestEnvVars:
|
class TestEnvVars:
|
||||||
def teardown_method(self):
|
def teardown_method(self) -> None:
|
||||||
# Restore default values
|
# Restore default values
|
||||||
Image.core.set_alignment(1)
|
Image.core.set_alignment(1)
|
||||||
Image.core.set_block_size(1024 * 1024)
|
Image.core.set_block_size(1024 * 1024)
|
||||||
Image.core.set_blocks_max(0)
|
Image.core.set_blocks_max(0)
|
||||||
Image.core.clear_cache()
|
Image.core.clear_cache()
|
||||||
|
|
||||||
def test_units(self):
|
def test_units(self) -> None:
|
||||||
Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"})
|
Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"})
|
||||||
assert Image.core.get_blocks_max() == 2 * 1024
|
assert Image.core.get_blocks_max() == 2 * 1024
|
||||||
Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"})
|
Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"})
|
||||||
|
@ -186,6 +187,6 @@ class TestEnvVars:
|
||||||
{"PILLOW_BLOCKS_MAX": "wat"},
|
{"PILLOW_BLOCKS_MAX": "wat"},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_warnings(self, var):
|
def test_warnings(self, var: dict[str, str]) -> None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
Image._apply_env_variables(var)
|
Image._apply_env_variables(var)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
@ -11,16 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
|
||||||
|
|
||||||
|
|
||||||
class TestDecompressionBomb:
|
class TestDecompressionBomb:
|
||||||
def teardown_method(self, method):
|
def teardown_method(self, method) -> None:
|
||||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||||
|
|
||||||
def test_no_warning_small_file(self):
|
def test_no_warning_small_file(self) -> None:
|
||||||
# Implicit assert: no warning.
|
# Implicit assert: no warning.
|
||||||
# A warning would cause a failure.
|
# A warning would cause a failure.
|
||||||
with Image.open(TEST_FILE):
|
with Image.open(TEST_FILE):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_no_warning_no_limit(self):
|
def test_no_warning_no_limit(self) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
# Turn limit off
|
# Turn limit off
|
||||||
Image.MAX_IMAGE_PIXELS = None
|
Image.MAX_IMAGE_PIXELS = None
|
||||||
|
@ -32,7 +33,7 @@ class TestDecompressionBomb:
|
||||||
with Image.open(TEST_FILE):
|
with Image.open(TEST_FILE):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_warning(self):
|
def test_warning(self) -> None:
|
||||||
# Set limit to trigger warning on the test file
|
# Set limit to trigger warning on the test file
|
||||||
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
|
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
|
||||||
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
|
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
|
||||||
|
@ -41,7 +42,7 @@ class TestDecompressionBomb:
|
||||||
with Image.open(TEST_FILE):
|
with Image.open(TEST_FILE):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_exception(self):
|
def test_exception(self) -> None:
|
||||||
# Set limit to trigger exception on the test file
|
# Set limit to trigger exception on the test file
|
||||||
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
|
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
|
||||||
assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1
|
assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1
|
||||||
|
@ -50,22 +51,22 @@ class TestDecompressionBomb:
|
||||||
with Image.open(TEST_FILE):
|
with Image.open(TEST_FILE):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_exception_ico(self):
|
def test_exception_ico(self) -> None:
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
with Image.open("Tests/images/decompression_bomb.ico"):
|
with Image.open("Tests/images/decompression_bomb.ico"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_exception_gif(self):
|
def test_exception_gif(self) -> None:
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
with Image.open("Tests/images/decompression_bomb.gif"):
|
with Image.open("Tests/images/decompression_bomb.gif"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_exception_gif_extents(self):
|
def test_exception_gif_extents(self) -> None:
|
||||||
with Image.open("Tests/images/decompression_bomb_extents.gif") as im:
|
with Image.open("Tests/images/decompression_bomb_extents.gif") as im:
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
|
||||||
def test_exception_gif_zero_width(self):
|
def test_exception_gif_zero_width(self) -> None:
|
||||||
# Set limit to trigger exception on the test file
|
# Set limit to trigger exception on the test file
|
||||||
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
|
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
|
||||||
assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
|
assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
|
||||||
|
@ -74,7 +75,7 @@ class TestDecompressionBomb:
|
||||||
with Image.open("Tests/images/zero_width.gif"):
|
with Image.open("Tests/images/zero_width.gif"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_exception_bmp(self):
|
def test_exception_bmp(self) -> None:
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
|
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
|
||||||
pass
|
pass
|
||||||
|
@ -82,15 +83,15 @@ class TestDecompressionBomb:
|
||||||
|
|
||||||
class TestDecompressionCrop:
|
class TestDecompressionCrop:
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls) -> None:
|
||||||
width, height = 128, 128
|
width, height = 128, 128
|
||||||
Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
|
Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def teardown_class(cls):
|
def teardown_class(cls) -> None:
|
||||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||||
|
|
||||||
def test_enlarge_crop(self):
|
def test_enlarge_crop(self) -> None:
|
||||||
# Crops can extend the extents, therefore we should have the
|
# Crops can extend the extents, therefore we should have the
|
||||||
# same decompression bomb warnings on them.
|
# same decompression bomb warnings on them.
|
||||||
with hopper() as src:
|
with hopper() as src:
|
||||||
|
@ -98,7 +99,7 @@ class TestDecompressionCrop:
|
||||||
with pytest.warns(Image.DecompressionBombWarning):
|
with pytest.warns(Image.DecompressionBombWarning):
|
||||||
src.crop(box)
|
src.crop(box)
|
||||||
|
|
||||||
def test_crop_decompression_checks(self):
|
def test_crop_decompression_checks(self) -> None:
|
||||||
im = Image.new("RGB", (100, 100))
|
im = Image.new("RGB", (100, 100))
|
||||||
|
|
||||||
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):
|
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import _deprecate
|
from PIL import _deprecate
|
||||||
|
@ -19,12 +20,12 @@ from PIL import _deprecate
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_version(version, expected):
|
def test_version(version: int | None, expected: str) -> None:
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old thing", version, "new thing")
|
_deprecate.deprecate("Old thing", version, "new thing")
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_version():
|
def test_unknown_version() -> None:
|
||||||
expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?"
|
expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?"
|
||||||
with pytest.raises(ValueError, match=expected):
|
with pytest.raises(ValueError, match=expected):
|
||||||
_deprecate.deprecate("Old thing", 12345, "new thing")
|
_deprecate.deprecate("Old thing", 12345, "new thing")
|
||||||
|
@ -45,13 +46,13 @@ def test_unknown_version():
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_old_version(deprecated, plural, expected):
|
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
|
||||||
expected = r""
|
expected = r""
|
||||||
with pytest.raises(RuntimeError, match=expected):
|
with pytest.raises(RuntimeError, match=expected):
|
||||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||||
|
|
||||||
|
|
||||||
def test_plural():
|
def test_plural() -> None:
|
||||||
expected = (
|
expected = (
|
||||||
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||||
r"Use new thing instead\."
|
r"Use new thing instead\."
|
||||||
|
@ -60,7 +61,7 @@ def test_plural():
|
||||||
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
|
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
|
||||||
|
|
||||||
|
|
||||||
def test_replacement_and_action():
|
def test_replacement_and_action() -> None:
|
||||||
expected = "Use only one of 'replacement' and 'action'"
|
expected = "Use only one of 'replacement' and 'action'"
|
||||||
with pytest.raises(ValueError, match=expected):
|
with pytest.raises(ValueError, match=expected):
|
||||||
_deprecate.deprecate(
|
_deprecate.deprecate(
|
||||||
|
@ -75,7 +76,7 @@ def test_replacement_and_action():
|
||||||
"Upgrade to new thing.",
|
"Upgrade to new thing.",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_action(action):
|
def test_action(action: str) -> None:
|
||||||
expected = (
|
expected = (
|
||||||
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||||
r"Upgrade to new thing\."
|
r"Upgrade to new thing\."
|
||||||
|
@ -84,7 +85,7 @@ def test_action(action):
|
||||||
_deprecate.deprecate("Old thing", 11, action=action)
|
_deprecate.deprecate("Old thing", 11, action=action)
|
||||||
|
|
||||||
|
|
||||||
def test_no_replacement_or_action():
|
def test_no_replacement_or_action() -> None:
|
||||||
expected = (
|
expected = (
|
||||||
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
|
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -14,7 +16,7 @@ except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_check():
|
def test_check() -> None:
|
||||||
# Check the correctness of the convenience function
|
# Check the correctness of the convenience function
|
||||||
for module in features.modules:
|
for module in features.modules:
|
||||||
assert features.check_module(module) == features.check(module)
|
assert features.check_module(module) == features.check(module)
|
||||||
|
@ -24,11 +26,11 @@ def test_check():
|
||||||
assert features.check_feature(feature) == features.check(feature)
|
assert features.check_feature(feature) == features.check(feature)
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
def test_version() -> None:
|
||||||
# Check the correctness of the convenience function
|
# Check the correctness of the convenience function
|
||||||
# and the format of version numbers
|
# and the format of version numbers
|
||||||
|
|
||||||
def test(name, function):
|
def test(name: str, function: Callable[[str], bool]) -> None:
|
||||||
version = features.version(name)
|
version = features.version(name)
|
||||||
if not features.check(name):
|
if not features.check(name):
|
||||||
assert version is None
|
assert version is None
|
||||||
|
@ -46,56 +48,56 @@ def test_version():
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("webp")
|
@skip_unless_feature("webp")
|
||||||
def test_webp_transparency():
|
def test_webp_transparency() -> None:
|
||||||
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha()
|
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha()
|
||||||
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY
|
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("webp")
|
@skip_unless_feature("webp")
|
||||||
def test_webp_mux():
|
def test_webp_mux() -> None:
|
||||||
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX
|
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("webp")
|
@skip_unless_feature("webp")
|
||||||
def test_webp_anim():
|
def test_webp_anim() -> None:
|
||||||
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM
|
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("libjpeg_turbo")
|
@skip_unless_feature("libjpeg_turbo")
|
||||||
def test_libjpeg_turbo_version():
|
def test_libjpeg_turbo_version() -> None:
|
||||||
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
|
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("libimagequant")
|
@skip_unless_feature("libimagequant")
|
||||||
def test_libimagequant_version():
|
def test_libimagequant_version() -> None:
|
||||||
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
|
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("feature", features.modules)
|
@pytest.mark.parametrize("feature", features.modules)
|
||||||
def test_check_modules(feature):
|
def test_check_modules(feature: str) -> None:
|
||||||
assert features.check_module(feature) in [True, False]
|
assert features.check_module(feature) in [True, False]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("feature", features.codecs)
|
@pytest.mark.parametrize("feature", features.codecs)
|
||||||
def test_check_codecs(feature):
|
def test_check_codecs(feature: str) -> None:
|
||||||
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() -> None:
|
||||||
with pytest.warns(UserWarning) as cm:
|
with pytest.warns(UserWarning) as cm:
|
||||||
has_feature = features.check("typo")
|
has_feature = features.check("typo")
|
||||||
assert has_feature is False
|
assert has_feature is False
|
||||||
assert str(cm[-1].message) == "Unknown feature 'typo'."
|
assert str(cm[-1].message) == "Unknown feature 'typo'."
|
||||||
|
|
||||||
|
|
||||||
def test_supported_modules():
|
def test_supported_modules() -> None:
|
||||||
assert isinstance(features.get_supported_modules(), list)
|
assert isinstance(features.get_supported_modules(), list)
|
||||||
assert isinstance(features.get_supported_codecs(), list)
|
assert isinstance(features.get_supported_codecs(), list)
|
||||||
assert isinstance(features.get_supported_features(), list)
|
assert isinstance(features.get_supported_features(), list)
|
||||||
assert isinstance(features.get_supported(), list)
|
assert isinstance(features.get_supported(), list)
|
||||||
|
|
||||||
|
|
||||||
def test_unsupported_codec():
|
def test_unsupported_codec() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
codec = "unsupported_codec"
|
codec = "unsupported_codec"
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
|
@ -105,7 +107,7 @@ def test_unsupported_codec():
|
||||||
features.version_codec(codec)
|
features.version_codec(codec)
|
||||||
|
|
||||||
|
|
||||||
def test_unsupported_module():
|
def test_unsupported_module() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
module = "unsupported_module"
|
module = "unsupported_module"
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
|
@ -115,7 +117,7 @@ def test_unsupported_module():
|
||||||
features.version_module(module)
|
features.version_module(module)
|
||||||
|
|
||||||
|
|
||||||
def test_pilinfo():
|
def test_pilinfo() -> None:
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
features.pilinfo(buf)
|
features.pilinfo(buf)
|
||||||
out = buf.getvalue()
|
out = buf.getvalue()
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageSequence, PngImagePlugin
|
from PIL import Image, ImageSequence, PngImagePlugin
|
||||||
|
@ -7,7 +10,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
|
||||||
# APNG browser support tests and fixtures via:
|
# APNG browser support tests and fixtures via:
|
||||||
# https://philip.html5.org/tests/apng/tests.html
|
# https://philip.html5.org/tests/apng/tests.html
|
||||||
# (referenced from https://wiki.mozilla.org/APNG_Specification)
|
# (referenced from https://wiki.mozilla.org/APNG_Specification)
|
||||||
def test_apng_basic():
|
def test_apng_basic() -> None:
|
||||||
with Image.open("Tests/images/apng/single_frame.png") as im:
|
with Image.open("Tests/images/apng/single_frame.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
@ -44,14 +47,14 @@ def test_apng_basic():
|
||||||
"filename",
|
"filename",
|
||||||
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
|
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
|
||||||
)
|
)
|
||||||
def test_apng_fdat(filename):
|
def test_apng_fdat(filename: str) -> None:
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) 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)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_dispose():
|
def test_apng_dispose() -> None:
|
||||||
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_none.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)
|
||||||
|
@ -83,7 +86,7 @@ def test_apng_dispose():
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_dispose_region():
|
def test_apng_dispose_region() -> None:
|
||||||
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_none_region.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)
|
||||||
|
@ -105,7 +108,7 @@ def test_apng_dispose_region():
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_dispose_op_previous_frame():
|
def test_apng_dispose_op_previous_frame() -> None:
|
||||||
# Test that the dispose settings being used are from the previous frame
|
# Test that the dispose settings being used are from the previous frame
|
||||||
#
|
#
|
||||||
# Image created with:
|
# Image created with:
|
||||||
|
@ -130,14 +133,14 @@ def test_apng_dispose_op_previous_frame():
|
||||||
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
|
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_dispose_op_background_p_mode():
|
def test_apng_dispose_op_background_p_mode() -> None:
|
||||||
with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
im.load()
|
im.load()
|
||||||
assert im.size == (128, 64)
|
assert im.size == (128, 64)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_blend():
|
def test_apng_blend() -> None:
|
||||||
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
|
with Image.open("Tests/images/apng/blend_op_source_solid.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)
|
||||||
|
@ -164,20 +167,20 @@ def test_apng_blend():
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_blend_transparency():
|
def test_apng_blend_transparency() -> None:
|
||||||
with Image.open("Tests/images/blend_transparency.png") as im:
|
with Image.open("Tests/images/blend_transparency.png") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.getpixel((0, 0)) == (255, 0, 0)
|
assert im.getpixel((0, 0)) == (255, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_chunk_order():
|
def test_apng_chunk_order() -> None:
|
||||||
with Image.open("Tests/images/apng/fctl_actl.png") as im:
|
with Image.open("Tests/images/apng/fctl_actl.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)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_delay():
|
def test_apng_delay() -> None:
|
||||||
with Image.open("Tests/images/apng/delay.png") as im:
|
with Image.open("Tests/images/apng/delay.png") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.info.get("duration") == 500.0
|
assert im.info.get("duration") == 500.0
|
||||||
|
@ -217,7 +220,7 @@ def test_apng_delay():
|
||||||
assert im.info.get("duration") == 1000.0
|
assert im.info.get("duration") == 1000.0
|
||||||
|
|
||||||
|
|
||||||
def test_apng_num_plays():
|
def test_apng_num_plays() -> None:
|
||||||
with Image.open("Tests/images/apng/num_plays.png") as im:
|
with Image.open("Tests/images/apng/num_plays.png") as im:
|
||||||
assert im.info.get("loop") == 0
|
assert im.info.get("loop") == 0
|
||||||
|
|
||||||
|
@ -225,7 +228,7 @@ def test_apng_num_plays():
|
||||||
assert im.info.get("loop") == 1
|
assert im.info.get("loop") == 1
|
||||||
|
|
||||||
|
|
||||||
def test_apng_mode():
|
def test_apng_mode() -> None:
|
||||||
with Image.open("Tests/images/apng/mode_16bit.png") as im:
|
with Image.open("Tests/images/apng/mode_16bit.png") as im:
|
||||||
assert im.mode == "RGBA"
|
assert im.mode == "RGBA"
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
@ -266,7 +269,7 @@ def test_apng_mode():
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 255, 128)
|
assert im.getpixel((64, 32)) == (0, 0, 255, 128)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_chunk_errors():
|
def test_apng_chunk_errors() -> None:
|
||||||
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
|
@ -291,7 +294,7 @@ def test_apng_chunk_errors():
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_syntax_errors():
|
def test_apng_syntax_errors() -> None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
@ -335,14 +338,14 @@ def test_apng_syntax_errors():
|
||||||
"sequence_fdat_fctl.png",
|
"sequence_fdat_fctl.png",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_apng_sequence_errors(test_file):
|
def test_apng_sequence_errors(test_file: str) -> None:
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save(tmp_path):
|
def test_apng_save(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/apng/single_frame.png") as im:
|
with Image.open("Tests/images/apng/single_frame.png") as im:
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
im.save(test_file, save_all=True)
|
im.save(test_file, save_all=True)
|
||||||
|
@ -373,7 +376,7 @@ def test_apng_save(tmp_path):
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_alpha(tmp_path):
|
def test_apng_save_alpha(tmp_path: Path) -> None:
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
|
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
|
||||||
|
@ -387,7 +390,7 @@ def test_apng_save_alpha(tmp_path):
|
||||||
assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)
|
assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_split_fdat(tmp_path):
|
def test_apng_save_split_fdat(tmp_path: Path) -> None:
|
||||||
# test to make sure we do not generate sequence errors when writing
|
# test to make sure we do not generate sequence errors when writing
|
||||||
# frames with image data spanning multiple fdAT chunks (in this case
|
# frames with image data spanning multiple fdAT chunks (in this case
|
||||||
# both the default image and first animation frame will span multiple
|
# both the default image and first animation frame will span multiple
|
||||||
|
@ -411,7 +414,7 @@ def test_apng_save_split_fdat(tmp_path):
|
||||||
assert exception is None
|
assert exception is None
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_duration_loop(tmp_path):
|
def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
with Image.open("Tests/images/apng/delay.png") as im:
|
with Image.open("Tests/images/apng/delay.png") as im:
|
||||||
frames = []
|
frames = []
|
||||||
|
@ -474,7 +477,7 @@ def test_apng_save_duration_loop(tmp_path):
|
||||||
assert im.info["duration"] == 600
|
assert im.info["duration"] == 600
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_disposal(tmp_path):
|
def test_apng_save_disposal(tmp_path: Path) -> None:
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
size = (128, 64)
|
size = (128, 64)
|
||||||
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
||||||
|
@ -575,7 +578,7 @@ def test_apng_save_disposal(tmp_path):
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_disposal_previous(tmp_path):
|
def test_apng_save_disposal_previous(tmp_path: Path) -> None:
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
size = (128, 64)
|
size = (128, 64)
|
||||||
blue = Image.new("RGBA", size, (0, 0, 255, 255))
|
blue = Image.new("RGBA", size, (0, 0, 255, 255))
|
||||||
|
@ -597,7 +600,7 @@ def test_apng_save_disposal_previous(tmp_path):
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_blend(tmp_path):
|
def test_apng_save_blend(tmp_path: Path) -> None:
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
size = (128, 64)
|
size = (128, 64)
|
||||||
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
||||||
|
@ -665,7 +668,7 @@ def test_apng_save_blend(tmp_path):
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_seek_after_close():
|
def test_seek_after_close() -> None:
|
||||||
im = Image.open("Tests/images/apng/delay.png")
|
im = Image.open("Tests/images/apng/delay.png")
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
im.close()
|
im.close()
|
||||||
|
@ -677,7 +680,9 @@ def test_seek_after_close():
|
||||||
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
||||||
@pytest.mark.parametrize("default_image", (True, False))
|
@pytest.mark.parametrize("default_image", (True, False))
|
||||||
@pytest.mark.parametrize("duplicate", (True, False))
|
@pytest.mark.parametrize("duplicate", (True, False))
|
||||||
def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
|
def test_different_modes_in_later_frames(
|
||||||
|
mode: str, default_image: bool, duplicate: bool, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
im = Image.new("L", (1, 1))
|
im = Image.new("L", (1, 1))
|
||||||
|
@ -689,3 +694,12 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat
|
||||||
)
|
)
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
assert reloaded.mode == mode
|
assert reloaded.mode == mode
|
||||||
|
|
||||||
|
|
||||||
|
def test_apng_repeated_seeks_give_correct_info() -> None:
|
||||||
|
with Image.open("Tests/images/apng/different_durations.png") as im:
|
||||||
|
for i in range(3):
|
||||||
|
im.seek(0)
|
||||||
|
assert im.info["duration"] == 4000
|
||||||
|
im.seek(1)
|
||||||
|
assert im.info["duration"] == 1000
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
@ -11,7 +14,7 @@ from .helper import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_load_blp1():
|
def test_load_blp1() -> None:
|
||||||
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")
|
||||||
|
|
||||||
|
@ -19,22 +22,22 @@ def test_load_blp1():
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_load_blp2_raw():
|
def test_load_blp2_raw() -> None:
|
||||||
with Image.open("Tests/images/blp/blp2_raw.blp") as im:
|
with Image.open("Tests/images/blp/blp2_raw.blp") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png")
|
assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png")
|
||||||
|
|
||||||
|
|
||||||
def test_load_blp2_dxt1():
|
def test_load_blp2_dxt1() -> None:
|
||||||
with Image.open("Tests/images/blp/blp2_dxt1.blp") as im:
|
with Image.open("Tests/images/blp/blp2_dxt1.blp") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png")
|
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png")
|
||||||
|
|
||||||
|
|
||||||
def test_load_blp2_dxt1a():
|
def test_load_blp2_dxt1a() -> None:
|
||||||
with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
|
with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
|
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
|
||||||
|
|
||||||
|
|
||||||
def test_save(tmp_path):
|
def test_save(tmp_path: Path) -> None:
|
||||||
f = str(tmp_path / "temp.blp")
|
f = str(tmp_path / "temp.blp")
|
||||||
|
|
||||||
for version in ("BLP1", "BLP2"):
|
for version in ("BLP1", "BLP2"):
|
||||||
|
@ -68,7 +71,7 @@ def test_save(tmp_path):
|
||||||
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crashes(test_file):
|
def test_crashes(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -13,8 +15,8 @@ from .helper import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_sanity(tmp_path):
|
def test_sanity(tmp_path: Path) -> None:
|
||||||
def roundtrip(im):
|
def roundtrip(im: Image.Image) -> None:
|
||||||
outfile = str(tmp_path / "temp.bmp")
|
outfile = str(tmp_path / "temp.bmp")
|
||||||
|
|
||||||
im.save(outfile, "BMP")
|
im.save(outfile, "BMP")
|
||||||
|
@ -34,20 +36,20 @@ def test_sanity(tmp_path):
|
||||||
roundtrip(hopper("RGB"))
|
roundtrip(hopper("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
BmpImagePlugin.BmpImageFile(fp)
|
BmpImagePlugin.BmpImageFile(fp)
|
||||||
|
|
||||||
|
|
||||||
def test_fallback_if_mmap_errors():
|
def test_fallback_if_mmap_errors() -> None:
|
||||||
# This image has been truncated,
|
# This image has been truncated,
|
||||||
# so that the buffer is not large enough when using mmap
|
# so that the buffer is not large enough when using mmap
|
||||||
with Image.open("Tests/images/mmap_error.bmp") as im:
|
with Image.open("Tests/images/mmap_error.bmp") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
|
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
|
||||||
|
|
||||||
|
|
||||||
def test_save_to_bytes():
|
def test_save_to_bytes() -> None:
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.save(output, "BMP")
|
im.save(output, "BMP")
|
||||||
|
@ -59,7 +61,7 @@ def test_save_to_bytes():
|
||||||
assert reloaded.format == "BMP"
|
assert reloaded.format == "BMP"
|
||||||
|
|
||||||
|
|
||||||
def test_small_palette(tmp_path):
|
def test_small_palette(tmp_path: Path) -> None:
|
||||||
im = Image.new("P", (1, 1))
|
im = Image.new("P", (1, 1))
|
||||||
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
|
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
|
||||||
im.putpalette(colors)
|
im.putpalette(colors)
|
||||||
|
@ -71,7 +73,7 @@ def test_small_palette(tmp_path):
|
||||||
assert reloaded.getpalette() == colors
|
assert reloaded.getpalette() == colors
|
||||||
|
|
||||||
|
|
||||||
def test_save_too_large(tmp_path):
|
def test_save_too_large(tmp_path: Path) -> None:
|
||||||
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:
|
||||||
im._size = (37838, 37838)
|
im._size = (37838, 37838)
|
||||||
|
@ -79,7 +81,7 @@ def test_save_too_large(tmp_path):
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
|
|
||||||
def test_dpi():
|
def test_dpi() -> None:
|
||||||
dpi = (72, 72)
|
dpi = (72, 72)
|
||||||
|
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
|
@ -91,7 +93,7 @@ def test_dpi():
|
||||||
assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161)
|
assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161)
|
||||||
|
|
||||||
|
|
||||||
def test_save_bmp_with_dpi(tmp_path):
|
def test_save_bmp_with_dpi(tmp_path: Path) -> None:
|
||||||
# Test for #1301
|
# Test for #1301
|
||||||
# Arrange
|
# Arrange
|
||||||
outfile = str(tmp_path / "temp.jpg")
|
outfile = str(tmp_path / "temp.jpg")
|
||||||
|
@ -109,7 +111,7 @@ def test_save_bmp_with_dpi(tmp_path):
|
||||||
assert reloaded.format == "JPEG"
|
assert reloaded.format == "JPEG"
|
||||||
|
|
||||||
|
|
||||||
def test_save_float_dpi(tmp_path):
|
def test_save_float_dpi(tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.bmp")
|
outfile = str(tmp_path / "temp.bmp")
|
||||||
with Image.open("Tests/images/hopper.bmp") as im:
|
with Image.open("Tests/images/hopper.bmp") as im:
|
||||||
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
|
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
|
||||||
|
@ -117,7 +119,7 @@ def test_save_float_dpi(tmp_path):
|
||||||
assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306)
|
assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306)
|
||||||
|
|
||||||
|
|
||||||
def test_load_dib():
|
def test_load_dib() -> None:
|
||||||
# test for #1293, Imagegrab returning Unsupported Bitfields Format
|
# test for #1293, Imagegrab returning Unsupported Bitfields Format
|
||||||
with Image.open("Tests/images/clipboard.dib") as im:
|
with Image.open("Tests/images/clipboard.dib") as im:
|
||||||
assert im.format == "DIB"
|
assert im.format == "DIB"
|
||||||
|
@ -126,7 +128,7 @@ def test_load_dib():
|
||||||
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png")
|
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png")
|
||||||
|
|
||||||
|
|
||||||
def test_save_dib(tmp_path):
|
def test_save_dib(tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.dib")
|
outfile = str(tmp_path / "temp.dib")
|
||||||
|
|
||||||
with Image.open("Tests/images/clipboard.dib") as im:
|
with Image.open("Tests/images/clipboard.dib") as im:
|
||||||
|
@ -138,7 +140,7 @@ def test_save_dib(tmp_path):
|
||||||
assert_image_equal(im, reloaded)
|
assert_image_equal(im, reloaded)
|
||||||
|
|
||||||
|
|
||||||
def test_rgba_bitfields():
|
def test_rgba_bitfields() -> None:
|
||||||
# This test image has been manually hexedited
|
# This test image has been manually hexedited
|
||||||
# to change the bitfield compression in the header from XBGR to RGBA
|
# to change the bitfield compression in the header from XBGR to RGBA
|
||||||
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
|
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
|
||||||
|
@ -156,7 +158,7 @@ def test_rgba_bitfields():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_rle8():
|
def test_rle8() -> None:
|
||||||
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
||||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||||
|
|
||||||
|
@ -176,7 +178,7 @@ def test_rle8():
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_rle4():
|
def test_rle4() -> None:
|
||||||
with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
|
with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
|
||||||
assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12)
|
assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12)
|
||||||
|
|
||||||
|
@ -192,7 +194,7 @@ def test_rle4():
|
||||||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_rle8_eof(file_name, length):
|
def test_rle8_eof(file_name: str, length: int) -> None:
|
||||||
with open(file_name, "rb") as fp:
|
with open(file_name, "rb") as fp:
|
||||||
data = fp.read(length)
|
data = fp.read(length)
|
||||||
with Image.open(io.BytesIO(data)) as im:
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
@ -200,7 +202,7 @@ def test_rle8_eof(file_name, length):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_offset():
|
def test_offset() -> None:
|
||||||
# This image has been hexedited
|
# This image has been hexedited
|
||||||
# to exclude the palette size from the pixel data offset
|
# to exclude the palette size from the pixel data offset
|
||||||
with Image.open("Tests/images/pal8_offset.bmp") as im:
|
with Image.open("Tests/images/pal8_offset.bmp") as im:
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import BufrStubImagePlugin, Image
|
from PIL import BufrStubImagePlugin, Image
|
||||||
|
@ -8,7 +11,7 @@ from .helper import hopper
|
||||||
TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d"
|
TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d"
|
||||||
|
|
||||||
|
|
||||||
def test_open():
|
def test_open() -> None:
|
||||||
# Act
|
# Act
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
# Assert
|
# Assert
|
||||||
|
@ -19,7 +22,7 @@ def test_open():
|
||||||
assert im.size == (1, 1)
|
assert im.size == (1, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
@ -28,7 +31,7 @@ def test_invalid_file():
|
||||||
BufrStubImagePlugin.BufrStubImageFile(invalid_file)
|
BufrStubImagePlugin.BufrStubImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_load():
|
def test_load() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
# Act / Assert: stub cannot load without an implemented handler
|
# Act / Assert: stub cannot load without an implemented handler
|
||||||
|
@ -36,7 +39,7 @@ def test_load():
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_save(tmp_path):
|
def test_save(tmp_path: Path) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper()
|
im = hopper()
|
||||||
tmpfile = str(tmp_path / "temp.bufr")
|
tmpfile = str(tmp_path / "temp.bufr")
|
||||||
|
@ -46,13 +49,13 @@ def test_save(tmp_path):
|
||||||
im.save(tmpfile)
|
im.save(tmpfile)
|
||||||
|
|
||||||
|
|
||||||
def test_handler(tmp_path):
|
def test_handler(tmp_path: Path) -> None:
|
||||||
class TestHandler:
|
class TestHandler:
|
||||||
opened = False
|
opened = False
|
||||||
loaded = False
|
loaded = False
|
||||||
saved = False
|
saved = False
|
||||||
|
|
||||||
def open(self, im):
|
def open(self, im) -> None:
|
||||||
self.opened = True
|
self.opened = True
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
|
@ -60,7 +63,7 @@ def test_handler(tmp_path):
|
||||||
im.fp.close()
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename):
|
def save(self, im, fp, filename) -> None:
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
handler = TestHandler()
|
handler = TestHandler()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import ContainerIO, Image
|
from PIL import ContainerIO, Image
|
||||||
|
@ -8,19 +9,19 @@ from .helper import hopper
|
||||||
TEST_FILE = "Tests/images/dummy.container"
|
TEST_FILE = "Tests/images/dummy.container"
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
dir(Image)
|
dir(Image)
|
||||||
dir(ContainerIO)
|
dir(ContainerIO)
|
||||||
|
|
||||||
|
|
||||||
def test_isatty():
|
def test_isatty() -> None:
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
container = ContainerIO.ContainerIO(im, 0, 0)
|
container = ContainerIO.ContainerIO(im, 0, 0)
|
||||||
|
|
||||||
assert container.isatty() is False
|
assert container.isatty() is False
|
||||||
|
|
||||||
|
|
||||||
def test_seek_mode_0():
|
def test_seek_mode_0() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = 0
|
mode = 0
|
||||||
with open(TEST_FILE, "rb") as fh:
|
with open(TEST_FILE, "rb") as fh:
|
||||||
|
@ -34,7 +35,7 @@ def test_seek_mode_0():
|
||||||
assert container.tell() == 33
|
assert container.tell() == 33
|
||||||
|
|
||||||
|
|
||||||
def test_seek_mode_1():
|
def test_seek_mode_1() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = 1
|
mode = 1
|
||||||
with open(TEST_FILE, "rb") as fh:
|
with open(TEST_FILE, "rb") as fh:
|
||||||
|
@ -48,7 +49,7 @@ def test_seek_mode_1():
|
||||||
assert container.tell() == 66
|
assert container.tell() == 66
|
||||||
|
|
||||||
|
|
||||||
def test_seek_mode_2():
|
def test_seek_mode_2() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = 2
|
mode = 2
|
||||||
with open(TEST_FILE, "rb") as fh:
|
with open(TEST_FILE, "rb") as fh:
|
||||||
|
@ -63,7 +64,7 @@ def test_seek_mode_2():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
def test_read_n0(bytesmode):
|
def test_read_n0(bytesmode: bool) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||||
|
@ -79,7 +80,7 @@ def test_read_n0(bytesmode):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
def test_read_n(bytesmode):
|
def test_read_n(bytesmode: bool) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||||
|
@ -95,7 +96,7 @@ def test_read_n(bytesmode):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
def test_read_eof(bytesmode):
|
def test_read_eof(bytesmode: bool) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||||
|
@ -111,7 +112,7 @@ def test_read_eof(bytesmode):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
def test_readline(bytesmode):
|
def test_readline(bytesmode: bool) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||||
|
@ -126,7 +127,7 @@ def test_readline(bytesmode):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||||
def test_readlines(bytesmode):
|
def test_readlines(bytesmode: bool) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
expected = [
|
expected = [
|
||||||
"This is line 1\n",
|
"This is line 1\n",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import CurImagePlugin, Image
|
from PIL import CurImagePlugin, Image
|
||||||
|
@ -6,7 +7,7 @@ from PIL import CurImagePlugin, Image
|
||||||
TEST_FILE = "Tests/images/deerstalker.cur"
|
TEST_FILE = "Tests/images/deerstalker.cur"
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert im.size == (32, 32)
|
assert im.size == (32, 32)
|
||||||
assert isinstance(im, CurImagePlugin.CurImageFile)
|
assert isinstance(im, CurImagePlugin.CurImageFile)
|
||||||
|
@ -16,7 +17,7 @@ def test_sanity():
|
||||||
assert im.getpixel((16, 16)) == (84, 87, 86, 255)
|
assert im.getpixel((16, 16)) == (84, 87, 86, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -11,7 +12,7 @@ from .helper import assert_image_equal, hopper, is_pypy
|
||||||
TEST_FILE = "Tests/images/hopper.dcx"
|
TEST_FILE = "Tests/images/hopper.dcx"
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
@ -24,8 +25,8 @@ def test_sanity():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||||
def test_unclosed_file():
|
def test_unclosed_file() -> None:
|
||||||
def open():
|
def open() -> None:
|
||||||
im = Image.open(TEST_FILE)
|
im = Image.open(TEST_FILE)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -33,26 +34,26 @@ def test_unclosed_file():
|
||||||
open()
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file() -> None:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
im = Image.open(TEST_FILE)
|
im = Image.open(TEST_FILE)
|
||||||
im.load()
|
im.load()
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
|
||||||
def test_context_manager():
|
def test_context_manager() -> None:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
DcxImagePlugin.DcxImageFile(fp)
|
DcxImagePlugin.DcxImageFile(fp)
|
||||||
|
|
||||||
|
|
||||||
def test_tell():
|
def test_tell() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
# Act
|
# Act
|
||||||
|
@ -62,13 +63,13 @@ def test_tell():
|
||||||
assert frame == 0
|
assert frame == 0
|
||||||
|
|
||||||
|
|
||||||
def test_n_frames():
|
def test_n_frames() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror():
|
def test_eoferror() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
|
@ -81,7 +82,7 @@ def test_eoferror():
|
||||||
im.seek(n_frames - 1)
|
im.seek(n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
def test_seek_too_far():
|
def test_seek_too_far() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
frame = 999 # too big on purpose
|
frame = 999 # too big on purpose
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
"""Test DdsImagePlugin"""
|
"""Test DdsImagePlugin"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -32,6 +35,7 @@ TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SR
|
||||||
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
|
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
|
||||||
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
|
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
|
||||||
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
|
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
|
||||||
|
TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds"
|
||||||
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
|
||||||
TEST_FILE_DX10_BC1_TYPELESS,
|
TEST_FILE_DX10_BC1_TYPELESS,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_sanity_dxt1_bc1(image_path):
|
def test_sanity_dxt1_bc1(image_path: str) -> None:
|
||||||
"""Check DXT1 and BC1 images can be opened"""
|
"""Check DXT1 and BC1 images can be opened"""
|
||||||
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
|
with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
|
||||||
target = target.convert("RGBA")
|
target = target.convert("RGBA")
|
||||||
|
@ -58,7 +62,7 @@ def test_sanity_dxt1_bc1(image_path):
|
||||||
assert_image_equal(im, target)
|
assert_image_equal(im, target)
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_dxt3():
|
def test_sanity_dxt3() -> None:
|
||||||
"""Check DXT3 images can be opened"""
|
"""Check DXT3 images can be opened"""
|
||||||
|
|
||||||
with Image.open(TEST_FILE_DXT3) as im:
|
with Image.open(TEST_FILE_DXT3) as im:
|
||||||
|
@ -71,7 +75,7 @@ def test_sanity_dxt3():
|
||||||
assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_dxt5():
|
def test_sanity_dxt5() -> None:
|
||||||
"""Check DXT5 images can be opened"""
|
"""Check DXT5 images can be opened"""
|
||||||
|
|
||||||
with Image.open(TEST_FILE_DXT5) as im:
|
with Image.open(TEST_FILE_DXT5) as im:
|
||||||
|
@ -92,7 +96,7 @@ def test_sanity_dxt5():
|
||||||
TEST_FILE_BC4U,
|
TEST_FILE_BC4U,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_sanity_ati1_bc4u(image_path):
|
def test_sanity_ati1_bc4u(image_path: str) -> None:
|
||||||
"""Check ATI1 and BC4U images can be opened"""
|
"""Check ATI1 and BC4U images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -113,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path):
|
||||||
TEST_FILE_DX10_BC4_TYPELESS,
|
TEST_FILE_DX10_BC4_TYPELESS,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dx10_bc4(image_path):
|
def test_dx10_bc4(image_path: str) -> None:
|
||||||
"""Check DX10 BC4 images can be opened"""
|
"""Check DX10 BC4 images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -134,7 +138,7 @@ def test_dx10_bc4(image_path):
|
||||||
TEST_FILE_BC5U,
|
TEST_FILE_BC5U,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_sanity_ati2_bc5u(image_path):
|
def test_sanity_ati2_bc5u(image_path: str) -> None:
|
||||||
"""Check ATI2 and BC5U images can be opened"""
|
"""Check ATI2 and BC5U images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -158,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path):
|
||||||
(TEST_FILE_BC5S, TEST_FILE_BC5S),
|
(TEST_FILE_BC5S, TEST_FILE_BC5S),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_dx10_bc5(image_path, expected_path):
|
def test_dx10_bc5(image_path: str, expected_path: str) -> None:
|
||||||
"""Check DX10 BC5 images can be opened"""
|
"""Check DX10 BC5 images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -172,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
|
@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
|
||||||
def test_dx10_bc6h(image_path):
|
def test_dx10_bc6h(image_path: str) -> None:
|
||||||
"""Check DX10 BC6H/BC6HS images can be opened"""
|
"""Check DX10 BC6H/BC6HS images can be opened"""
|
||||||
|
|
||||||
with Image.open(image_path) as im:
|
with Image.open(image_path) as im:
|
||||||
|
@ -185,7 +189,7 @@ def test_dx10_bc6h(image_path):
|
||||||
assert_image_equal_tofile(im, image_path.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, image_path.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
def test_dx10_bc7():
|
def test_dx10_bc7() -> None:
|
||||||
"""Check DX10 images can be opened"""
|
"""Check DX10 images can be opened"""
|
||||||
|
|
||||||
with Image.open(TEST_FILE_DX10_BC7) as im:
|
with Image.open(TEST_FILE_DX10_BC7) as im:
|
||||||
|
@ -198,7 +202,7 @@ def test_dx10_bc7():
|
||||||
assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
def test_dx10_bc7_unorm_srgb():
|
def test_dx10_bc7_unorm_srgb() -> None:
|
||||||
"""Check DX10 unsigned normalized integer images can be opened"""
|
"""Check DX10 unsigned normalized integer images can be opened"""
|
||||||
|
|
||||||
with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im:
|
with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im:
|
||||||
|
@ -214,7 +218,7 @@ def test_dx10_bc7_unorm_srgb():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dx10_r8g8b8a8():
|
def test_dx10_r8g8b8a8() -> None:
|
||||||
"""Check DX10 images can be opened"""
|
"""Check DX10 images can be opened"""
|
||||||
|
|
||||||
with Image.open(TEST_FILE_DX10_R8G8B8A8) as im:
|
with Image.open(TEST_FILE_DX10_R8G8B8A8) as im:
|
||||||
|
@ -227,7 +231,7 @@ def test_dx10_r8g8b8a8():
|
||||||
assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
def test_dx10_r8g8b8a8_unorm_srgb():
|
def test_dx10_r8g8b8a8_unorm_srgb() -> None:
|
||||||
"""Check DX10 unsigned normalized integer images can be opened"""
|
"""Check DX10 unsigned normalized integer images can be opened"""
|
||||||
|
|
||||||
with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im:
|
with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im:
|
||||||
|
@ -249,10 +253,11 @@ def test_dx10_r8g8b8a8_unorm_srgb():
|
||||||
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
|
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
|
||||||
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
|
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
|
||||||
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
|
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
|
||||||
|
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
|
||||||
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
|
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_uncompressed(mode, size, test_file):
|
def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None:
|
||||||
"""Check uncompressed images can be opened"""
|
"""Check uncompressed images can be opened"""
|
||||||
|
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
@ -263,7 +268,7 @@ def test_uncompressed(mode, size, test_file):
|
||||||
assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
def test__accept_true():
|
def test__accept_true() -> None:
|
||||||
"""Check valid prefix"""
|
"""Check valid prefix"""
|
||||||
# Arrange
|
# Arrange
|
||||||
prefix = b"DDS etc"
|
prefix = b"DDS etc"
|
||||||
|
@ -275,7 +280,7 @@ def test__accept_true():
|
||||||
assert output
|
assert output
|
||||||
|
|
||||||
|
|
||||||
def test__accept_false():
|
def test__accept_false() -> None:
|
||||||
"""Check invalid prefix"""
|
"""Check invalid prefix"""
|
||||||
# Arrange
|
# Arrange
|
||||||
prefix = b"something invalid"
|
prefix = b"something invalid"
|
||||||
|
@ -287,19 +292,19 @@ def test__accept_false():
|
||||||
assert not output
|
assert not output
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
DdsImagePlugin.DdsImageFile(invalid_file)
|
DdsImagePlugin.DdsImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_short_header():
|
def test_short_header() -> None:
|
||||||
"""Check a short header"""
|
"""Check a short header"""
|
||||||
with open(TEST_FILE_DXT5, "rb") as f:
|
with open(TEST_FILE_DXT5, "rb") as f:
|
||||||
img_file = f.read()
|
img_file = f.read()
|
||||||
|
|
||||||
def short_header():
|
def short_header() -> None:
|
||||||
with Image.open(BytesIO(img_file[:119])):
|
with Image.open(BytesIO(img_file[:119])):
|
||||||
pass # pragma: no cover
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
@ -307,13 +312,13 @@ def test_short_header():
|
||||||
short_header()
|
short_header()
|
||||||
|
|
||||||
|
|
||||||
def test_short_file():
|
def test_short_file() -> None:
|
||||||
"""Check that the appropriate error is thrown for a short file"""
|
"""Check that the appropriate error is thrown for a short file"""
|
||||||
|
|
||||||
with open(TEST_FILE_DXT5, "rb") as f:
|
with open(TEST_FILE_DXT5, "rb") as f:
|
||||||
img_file = f.read()
|
img_file = f.read()
|
||||||
|
|
||||||
def short_file():
|
def short_file() -> None:
|
||||||
with Image.open(BytesIO(img_file[:-100])) as im:
|
with Image.open(BytesIO(img_file[:-100])) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -321,7 +326,7 @@ def test_short_file():
|
||||||
short_file()
|
short_file()
|
||||||
|
|
||||||
|
|
||||||
def test_dxt5_colorblock_alpha_issue_4142():
|
def test_dxt5_colorblock_alpha_issue_4142() -> None:
|
||||||
"""Check that colorblocks are decoded correctly in DXT5"""
|
"""Check that colorblocks are decoded correctly in DXT5"""
|
||||||
|
|
||||||
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
|
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
|
||||||
|
@ -336,21 +341,14 @@ def test_dxt5_colorblock_alpha_issue_4142():
|
||||||
assert px[2] != 0
|
assert px[2] != 0
|
||||||
|
|
||||||
|
|
||||||
def test_palette():
|
def test_palette() -> None:
|
||||||
with Image.open("Tests/images/palette.dds") as im:
|
with Image.open("Tests/images/palette.dds") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
def test_unsupported_bitcount() -> None:
|
||||||
"test_file",
|
|
||||||
(
|
|
||||||
"Tests/images/unsupported_bitcount_rgb.dds",
|
|
||||||
"Tests/images/unsupported_bitcount_luminance.dds",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_unsupported_bitcount(test_file):
|
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
with Image.open(test_file):
|
with Image.open("Tests/images/unsupported_bitcount.dds"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -361,13 +359,13 @@ def test_unsupported_bitcount(test_file):
|
||||||
"Tests/images/unimplemented_pfflags.dds",
|
"Tests/images/unimplemented_pfflags.dds",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_not_implemented(test_file):
|
def test_not_implemented(test_file: str) -> None:
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
with Image.open(test_file):
|
with Image.open(test_file):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_save_unsupported_mode(tmp_path):
|
def test_save_unsupported_mode(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.dds")
|
out = str(tmp_path / "temp.dds")
|
||||||
im = hopper("HSV")
|
im = hopper("HSV")
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
@ -383,7 +381,7 @@ def test_save_unsupported_mode(tmp_path):
|
||||||
("RGBA", "Tests/images/pil123rgba.png"),
|
("RGBA", "Tests/images/pil123rgba.png"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_save(mode, test_file, tmp_path):
|
def test_save(mode: str, test_file: str, tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.dds")
|
out = str(tmp_path / "temp.dds")
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
assert im.mode == mode
|
assert im.mode == mode
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -82,7 +84,7 @@ simple_eps_file_with_long_binary_data = (
|
||||||
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
|
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("scale", (1, 2))
|
@pytest.mark.parametrize("scale", (1, 2))
|
||||||
def test_sanity(filename, size, scale):
|
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
|
||||||
expected_size = tuple(s * scale for s in size)
|
expected_size = tuple(s * scale for s in size)
|
||||||
with Image.open(filename) as image:
|
with Image.open(filename) as image:
|
||||||
image.load(scale=scale)
|
image.load(scale=scale)
|
||||||
|
@ -92,7 +94,7 @@ def test_sanity(filename, size, scale):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_load():
|
def test_load() -> None:
|
||||||
with Image.open(FILE1) as im:
|
with Image.open(FILE1) as im:
|
||||||
assert im.load()[0, 0] == (255, 255, 255)
|
assert im.load()[0, 0] == (255, 255, 255)
|
||||||
|
|
||||||
|
@ -100,7 +102,7 @@ def test_load():
|
||||||
assert im.load()[0, 0] == (255, 255, 255)
|
assert im.load()[0, 0] == (255, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_binary():
|
def test_binary() -> None:
|
||||||
if HAS_GHOSTSCRIPT:
|
if HAS_GHOSTSCRIPT:
|
||||||
assert EpsImagePlugin.gs_binary is not None
|
assert EpsImagePlugin.gs_binary is not None
|
||||||
else:
|
else:
|
||||||
|
@ -114,41 +116,41 @@ def test_binary():
|
||||||
assert EpsImagePlugin.gs_windows_binary is not None
|
assert EpsImagePlugin.gs_windows_binary is not None
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
EpsImagePlugin.EpsImageFile(invalid_file)
|
EpsImagePlugin.EpsImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_binary_header_only():
|
def test_binary_header_only() -> None:
|
||||||
data = io.BytesIO(simple_binary_header)
|
data = io.BytesIO(simple_binary_header)
|
||||||
with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'):
|
with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'):
|
||||||
EpsImagePlugin.EpsImageFile(data)
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
def test_missing_version_comment(prefix):
|
def test_missing_version_comment(prefix: bytes) -> None:
|
||||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
EpsImagePlugin.EpsImageFile(data)
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
def test_missing_boundingbox_comment(prefix):
|
def test_missing_boundingbox_comment(prefix: bytes) -> None:
|
||||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
|
||||||
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
|
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
|
||||||
EpsImagePlugin.EpsImageFile(data)
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
def test_invalid_boundingbox_comment(prefix):
|
def test_invalid_boundingbox_comment(prefix: bytes) -> None:
|
||||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
|
||||||
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
|
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
|
||||||
EpsImagePlugin.EpsImageFile(data)
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix):
|
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None:
|
||||||
data = io.BytesIO(
|
data = io.BytesIO(
|
||||||
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
|
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
|
||||||
)
|
)
|
||||||
|
@ -159,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
def test_ascii_comment_too_long(prefix):
|
def test_ascii_comment_too_long(prefix: bytes) -> None:
|
||||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
|
||||||
with pytest.raises(SyntaxError, match="not an EPS file"):
|
with pytest.raises(SyntaxError, match="not an EPS file"):
|
||||||
EpsImagePlugin.EpsImageFile(data)
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
def test_long_binary_data(prefix):
|
def test_long_binary_data(prefix: bytes) -> None:
|
||||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
||||||
EpsImagePlugin.EpsImageFile(data)
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
def test_load_long_binary_data(prefix):
|
def test_load_long_binary_data(prefix: bytes) -> None:
|
||||||
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
||||||
with Image.open(data) as img:
|
with Image.open(data) as img:
|
||||||
img.load()
|
img.load()
|
||||||
|
@ -186,7 +188,7 @@ def test_load_long_binary_data(prefix):
|
||||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||||
)
|
)
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_cmyk():
|
def test_cmyk() -> None:
|
||||||
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
|
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
|
||||||
assert cmyk_image.mode == "CMYK"
|
assert cmyk_image.mode == "CMYK"
|
||||||
assert cmyk_image.size == (100, 100)
|
assert cmyk_image.size == (100, 100)
|
||||||
|
@ -202,7 +204,7 @@ def test_cmyk():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_showpage():
|
def test_showpage() -> None:
|
||||||
# See https://github.com/python-pillow/Pillow/issues/2615
|
# See https://github.com/python-pillow/Pillow/issues/2615
|
||||||
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
||||||
with Image.open("Tests/images/reqd_showpage.png") as target:
|
with Image.open("Tests/images/reqd_showpage.png") as target:
|
||||||
|
@ -213,7 +215,7 @@ def test_showpage():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_transparency():
|
def test_transparency() -> None:
|
||||||
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
|
||||||
plot_image.load(transparency=True)
|
plot_image.load(transparency=True)
|
||||||
assert plot_image.mode == "RGBA"
|
assert plot_image.mode == "RGBA"
|
||||||
|
@ -224,7 +226,7 @@ def test_transparency():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_file_object(tmp_path):
|
def test_file_object(tmp_path: Path) -> None:
|
||||||
# issue 479
|
# issue 479
|
||||||
with Image.open(FILE1) as image1:
|
with Image.open(FILE1) as image1:
|
||||||
with open(str(tmp_path / "temp.eps"), "wb") as fh:
|
with open(str(tmp_path / "temp.eps"), "wb") as fh:
|
||||||
|
@ -232,7 +234,7 @@ def test_file_object(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
@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() -> None:
|
||||||
with open(FILE1, "rb") as f:
|
with open(FILE1, "rb") as f:
|
||||||
img_bytes = io.BytesIO(f.read())
|
img_bytes = io.BytesIO(f.read())
|
||||||
|
|
||||||
|
@ -245,12 +247,12 @@ def test_bytesio_object():
|
||||||
assert_image_similar(img, image1_scale1_compare, 5)
|
assert_image_similar(img, image1_scale1_compare, 5)
|
||||||
|
|
||||||
|
|
||||||
def test_1_mode():
|
def test_1_mode() -> None:
|
||||||
with Image.open("Tests/images/1.eps") as im:
|
with Image.open("Tests/images/1.eps") as im:
|
||||||
assert im.mode == "1"
|
assert im.mode == "1"
|
||||||
|
|
||||||
|
|
||||||
def test_image_mode_not_supported(tmp_path):
|
def test_image_mode_not_supported(tmp_path: Path) -> None:
|
||||||
im = hopper("RGBA")
|
im = hopper("RGBA")
|
||||||
tmpfile = str(tmp_path / "temp.eps")
|
tmpfile = str(tmp_path / "temp.eps")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -259,7 +261,7 @@ def test_image_mode_not_supported(tmp_path):
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
@skip_unless_feature("zlib")
|
@skip_unless_feature("zlib")
|
||||||
def test_render_scale1():
|
def test_render_scale1() -> None:
|
||||||
# We need png support for these render test
|
# We need png support for these render test
|
||||||
|
|
||||||
# Zero bounding box
|
# Zero bounding box
|
||||||
|
@ -270,7 +272,7 @@ def test_render_scale1():
|
||||||
image1_scale1_compare.load()
|
image1_scale1_compare.load()
|
||||||
assert_image_similar(image1_scale1, image1_scale1_compare, 5)
|
assert_image_similar(image1_scale1, image1_scale1_compare, 5)
|
||||||
|
|
||||||
# Non-Zero bounding box
|
# Non-zero bounding box
|
||||||
with Image.open(FILE2) as image2_scale1:
|
with Image.open(FILE2) as image2_scale1:
|
||||||
image2_scale1.load()
|
image2_scale1.load()
|
||||||
with Image.open(FILE2_COMPARE) as image2_scale1_compare:
|
with Image.open(FILE2_COMPARE) as image2_scale1_compare:
|
||||||
|
@ -281,7 +283,7 @@ def test_render_scale1():
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
@skip_unless_feature("zlib")
|
@skip_unless_feature("zlib")
|
||||||
def test_render_scale2():
|
def test_render_scale2() -> None:
|
||||||
# We need png support for these render test
|
# We need png support for these render test
|
||||||
|
|
||||||
# Zero bounding box
|
# Zero bounding box
|
||||||
|
@ -292,7 +294,7 @@ def test_render_scale2():
|
||||||
image1_scale2_compare.load()
|
image1_scale2_compare.load()
|
||||||
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
|
assert_image_similar(image1_scale2, image1_scale2_compare, 5)
|
||||||
|
|
||||||
# Non-Zero bounding box
|
# Non-zero bounding box
|
||||||
with Image.open(FILE2) as image2_scale2:
|
with Image.open(FILE2) as image2_scale2:
|
||||||
image2_scale2.load(scale=2)
|
image2_scale2.load(scale=2)
|
||||||
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
|
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
|
||||||
|
@ -303,7 +305,7 @@ def test_render_scale2():
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
|
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
|
||||||
def test_resize(filename):
|
def test_resize(filename: str) -> None:
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
new_size = (100, 100)
|
new_size = (100, 100)
|
||||||
im = im.resize(new_size)
|
im = im.resize(new_size)
|
||||||
|
@ -312,7 +314,7 @@ def test_resize(filename):
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
@pytest.mark.parametrize("filename", (FILE1, FILE2))
|
@pytest.mark.parametrize("filename", (FILE1, FILE2))
|
||||||
def test_thumbnail(filename):
|
def test_thumbnail(filename: str) -> None:
|
||||||
# Issue #619
|
# Issue #619
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
new_size = (100, 100)
|
new_size = (100, 100)
|
||||||
|
@ -320,20 +322,20 @@ def test_thumbnail(filename):
|
||||||
assert max(im.size) == max(new_size)
|
assert max(im.size) == max(new_size)
|
||||||
|
|
||||||
|
|
||||||
def test_read_binary_preview():
|
def test_read_binary_preview() -> None:
|
||||||
# Issue 302
|
# Issue 302
|
||||||
# open image with binary preview
|
# open image with binary preview
|
||||||
with Image.open(FILE3):
|
with Image.open(FILE3):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_readline_psfile(tmp_path):
|
def test_readline_psfile(tmp_path: Path) -> None:
|
||||||
# check all the freaking line endings possible from the spec
|
# check all the freaking line endings possible from the spec
|
||||||
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
|
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
|
||||||
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
||||||
strings = ["something", "else", "baz", "bif"]
|
strings = ["something", "else", "baz", "bif"]
|
||||||
|
|
||||||
def _test_readline(t, ending):
|
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
|
||||||
ending = "Failure with line ending: %s" % (
|
ending = "Failure with line ending: %s" % (
|
||||||
"".join("%s" % ord(s) for s in ending)
|
"".join("%s" % ord(s) for s in ending)
|
||||||
)
|
)
|
||||||
|
@ -342,13 +344,13 @@ def test_readline_psfile(tmp_path):
|
||||||
assert t.readline().strip("\r\n") == "baz", ending
|
assert t.readline().strip("\r\n") == "baz", ending
|
||||||
assert t.readline().strip("\r\n") == "bif", ending
|
assert t.readline().strip("\r\n") == "bif", ending
|
||||||
|
|
||||||
def _test_readline_io_psfile(test_string, ending):
|
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
|
||||||
f = io.BytesIO(test_string.encode("latin-1"))
|
f = io.BytesIO(test_string.encode("latin-1"))
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
t = EpsImagePlugin.PSFile(f)
|
t = EpsImagePlugin.PSFile(f)
|
||||||
_test_readline(t, ending)
|
_test_readline(t, ending)
|
||||||
|
|
||||||
def _test_readline_file_psfile(test_string, ending):
|
def _test_readline_file_psfile(test_string: str, ending: str) -> None:
|
||||||
f = str(tmp_path / "temp.txt")
|
f = str(tmp_path / "temp.txt")
|
||||||
with open(f, "wb") as w:
|
with open(f, "wb") as w:
|
||||||
w.write(test_string.encode("latin-1"))
|
w.write(test_string.encode("latin-1"))
|
||||||
|
@ -364,7 +366,7 @@ def test_readline_psfile(tmp_path):
|
||||||
_test_readline_file_psfile(s, ending)
|
_test_readline_file_psfile(s, ending)
|
||||||
|
|
||||||
|
|
||||||
def test_psfile_deprecation():
|
def test_psfile_deprecation() -> None:
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
EpsImagePlugin.PSFile(None)
|
EpsImagePlugin.PSFile(None)
|
||||||
|
|
||||||
|
@ -374,7 +376,7 @@ def test_psfile_deprecation():
|
||||||
"line_ending",
|
"line_ending",
|
||||||
(b"\r\n", b"\n", b"\n\r", b"\r"),
|
(b"\r\n", b"\n", b"\n\r", b"\r"),
|
||||||
)
|
)
|
||||||
def test_readline(prefix, line_ending):
|
def test_readline(prefix: bytes, line_ending: bytes) -> None:
|
||||||
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
|
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
|
||||||
data = io.BytesIO(simple_file)
|
data = io.BytesIO(simple_file)
|
||||||
test_file = EpsImagePlugin.EpsImageFile(data)
|
test_file = EpsImagePlugin.EpsImageFile(data)
|
||||||
|
@ -392,14 +394,14 @@ def test_readline(prefix, line_ending):
|
||||||
"Tests/images/illuCS6_preview.eps",
|
"Tests/images/illuCS6_preview.eps",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_open_eps(filename):
|
def test_open_eps(filename: str) -> None:
|
||||||
# https://github.com/python-pillow/Pillow/issues/1104
|
# 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")
|
||||||
def test_emptyline():
|
def test_emptyline() -> None:
|
||||||
# Test file includes an empty line in the header data
|
# Test file includes an empty line in the header data
|
||||||
emptyline_file = "Tests/images/zero_bb_emptyline.eps"
|
emptyline_file = "Tests/images/zero_bb_emptyline.eps"
|
||||||
|
|
||||||
|
@ -415,14 +417,14 @@ def test_emptyline():
|
||||||
"test_file",
|
"test_file",
|
||||||
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
|
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
|
||||||
)
|
)
|
||||||
def test_timeout(test_file):
|
def test_timeout(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with pytest.raises(Image.UnidentifiedImageError):
|
with pytest.raises(Image.UnidentifiedImageError):
|
||||||
with Image.open(f):
|
with Image.open(f):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_bounding_box_in_trailer():
|
def test_bounding_box_in_trailer() -> None:
|
||||||
# Check bounding boxes are parsed in the same way
|
# Check bounding boxes are parsed in the same way
|
||||||
# when specified in the header and the trailer
|
# when specified in the header and the trailer
|
||||||
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
|
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
|
||||||
|
@ -431,7 +433,7 @@ def test_bounding_box_in_trailer():
|
||||||
assert trailer_image.size == header_image.size
|
assert trailer_image.size == header_image.size
|
||||||
|
|
||||||
|
|
||||||
def test_eof_before_bounding_box():
|
def test_eof_before_bounding_box() -> None:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper
|
||||||
TEST_FILE = "Tests/images/hopper.fits"
|
TEST_FILE = "Tests/images/hopper.fits"
|
||||||
|
|
||||||
|
|
||||||
def test_open():
|
def test_open() -> None:
|
||||||
# Act
|
# Act
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
# Assert
|
# Assert
|
||||||
|
@ -21,7 +22,7 @@ def test_open():
|
||||||
assert_image_equal(im, hopper("L"))
|
assert_image_equal(im, hopper("L"))
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
@ -30,14 +31,14 @@ def test_invalid_file():
|
||||||
FitsImagePlugin.FitsImageFile(invalid_file)
|
FitsImagePlugin.FitsImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_truncated_fits():
|
def test_truncated_fits() -> None:
|
||||||
# No END to headers
|
# No END to headers
|
||||||
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
|
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
|
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
|
||||||
|
|
||||||
|
|
||||||
def test_naxis_zero():
|
def test_naxis_zero() -> None:
|
||||||
# This test image has been manually hexedited
|
# This test image has been manually hexedited
|
||||||
# to set the number of data axes to zero
|
# to set the number of data axes to zero
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
|
@ -45,7 +46,7 @@ def test_naxis_zero():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_comment():
|
def test_comment() -> None:
|
||||||
image_data = b"SIMPLE = T / comment string"
|
image_data = b"SIMPLE = T / comment string"
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
|
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import FliImagePlugin, Image
|
from PIL import FliImagePlugin, Image, ImageFile
|
||||||
|
|
||||||
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
||||||
|
|
||||||
|
@ -11,11 +12,14 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
||||||
# save as...-> hopper.fli, default options.
|
# save as...-> hopper.fli, default options.
|
||||||
static_test_file = "Tests/images/hopper.fli"
|
static_test_file = "Tests/images/hopper.fli"
|
||||||
|
|
||||||
# From https://samples.libav.org/fli-flc/
|
# From https://samples.ffmpeg.org/fli-flc/
|
||||||
animated_test_file = "Tests/images/a.fli"
|
animated_test_file = "Tests/images/a.fli"
|
||||||
|
|
||||||
|
# From https://samples.ffmpeg.org/fli-flc/
|
||||||
|
animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
|
||||||
|
|
||||||
def test_sanity():
|
|
||||||
|
def test_sanity() -> None:
|
||||||
with Image.open(static_test_file) as im:
|
with Image.open(static_test_file) as im:
|
||||||
im.load()
|
im.load()
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
|
@ -31,9 +35,27 @@ def test_sanity():
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefix_chunk() -> None:
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
try:
|
||||||
|
with Image.open(animated_test_file_with_prefix_chunk) as im:
|
||||||
|
assert im.mode == "P"
|
||||||
|
assert im.size == (320, 200)
|
||||||
|
assert im.format == "FLI"
|
||||||
|
assert im.info["duration"] == 171
|
||||||
|
assert im.is_animated
|
||||||
|
|
||||||
|
palette = im.getpalette()
|
||||||
|
assert palette[3:6] == [255, 255, 255]
|
||||||
|
assert palette[381:384] == [204, 204, 12]
|
||||||
|
assert palette[765:] == [252, 0, 0]
|
||||||
|
finally:
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||||
def test_unclosed_file():
|
def test_unclosed_file() -> None:
|
||||||
def open():
|
def open() -> None:
|
||||||
im = Image.open(static_test_file)
|
im = Image.open(static_test_file)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -41,14 +63,14 @@ def test_unclosed_file():
|
||||||
open()
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file() -> None:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
im = Image.open(static_test_file)
|
im = Image.open(static_test_file)
|
||||||
im.load()
|
im.load()
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
|
||||||
def test_seek_after_close():
|
def test_seek_after_close() -> None:
|
||||||
im = Image.open(animated_test_file)
|
im = Image.open(animated_test_file)
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
im.close()
|
im.close()
|
||||||
|
@ -57,13 +79,13 @@ def test_seek_after_close():
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
|
|
||||||
|
|
||||||
def test_context_manager():
|
def test_context_manager() -> None:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
with Image.open(static_test_file) as im:
|
with Image.open(static_test_file) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_tell():
|
def test_tell() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open(static_test_file) as im:
|
with Image.open(static_test_file) as im:
|
||||||
# Act
|
# Act
|
||||||
|
@ -73,20 +95,20 @@ def test_tell():
|
||||||
assert frame == 0
|
assert frame == 0
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
FliImagePlugin.FliImageFile(invalid_file)
|
FliImagePlugin.FliImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_palette_chunk_second():
|
def test_palette_chunk_second() -> None:
|
||||||
with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
|
with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
|
||||||
with Image.open(static_test_file) as expected:
|
with Image.open(static_test_file) as expected:
|
||||||
assert_image_equal(im.convert("RGB"), expected.convert("RGB"))
|
assert_image_equal(im.convert("RGB"), expected.convert("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_n_frames():
|
def test_n_frames() -> None:
|
||||||
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
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
@ -96,7 +118,7 @@ def test_n_frames():
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror():
|
def test_eoferror() -> None:
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
|
@ -109,7 +131,7 @@ def test_eoferror():
|
||||||
im.seek(n_frames - 1)
|
im.seek(n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
def test_seek_tell():
|
def test_seek_tell() -> None:
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
layer_number = im.tell()
|
layer_number = im.tell()
|
||||||
assert layer_number == 0
|
assert layer_number == 0
|
||||||
|
@ -131,7 +153,7 @@ def test_seek_tell():
|
||||||
assert layer_number == 1
|
assert layer_number == 1
|
||||||
|
|
||||||
|
|
||||||
def test_seek():
|
def test_seek() -> None:
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
im.seek(50)
|
im.seek(50)
|
||||||
|
|
||||||
|
@ -146,7 +168,7 @@ def test_seek():
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.timeout(timeout=3)
|
@pytest.mark.timeout(timeout=3)
|
||||||
def test_timeouts(test_file):
|
def test_timeouts(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
@ -159,7 +181,7 @@ def test_timeouts(test_file):
|
||||||
"Tests/images/crash-5762152299364352.fli",
|
"Tests/images/crash-5762152299364352.fli",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_crash(test_file):
|
def test_crash(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
@ -10,7 +11,7 @@ FpxImagePlugin = pytest.importorskip(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||||
assert im.mode == "L"
|
assert im.mode == "L"
|
||||||
assert im.size == (70, 46)
|
assert im.size == (70, 46)
|
||||||
|
@ -19,7 +20,7 @@ def test_sanity():
|
||||||
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
|
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
|
||||||
|
|
||||||
|
|
||||||
def test_close():
|
def test_close() -> None:
|
||||||
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||||
pass
|
pass
|
||||||
assert im.ole.fp.closed
|
assert im.ole.fp.closed
|
||||||
|
@ -29,7 +30,7 @@ def test_close():
|
||||||
assert im.ole.fp.closed
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
# Test an invalid OLE file
|
# Test an invalid OLE file
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
|
@ -41,7 +42,7 @@ def test_invalid_file():
|
||||||
FpxImagePlugin.FpxImageFile(ole_file)
|
FpxImagePlugin.FpxImageFile(ole_file)
|
||||||
|
|
||||||
|
|
||||||
def test_fpx_invalid_number_of_bands():
|
def test_fpx_invalid_number_of_bands() -> None:
|
||||||
with pytest.raises(OSError, match="Invalid number of bands"):
|
with pytest.raises(OSError, match="Invalid number of bands"):
|
||||||
with Image.open("Tests/images/input_bw_five_bands.fpx"):
|
with Image.open("Tests/images/input_bw_five_bands.fpx"):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import FtexImagePlugin, Image
|
from PIL import FtexImagePlugin, Image
|
||||||
|
@ -6,18 +7,18 @@ from PIL import FtexImagePlugin, Image
|
||||||
from .helper import assert_image_equal_tofile, assert_image_similar
|
from .helper import assert_image_equal_tofile, assert_image_similar
|
||||||
|
|
||||||
|
|
||||||
def test_load_raw():
|
def test_load_raw() -> None:
|
||||||
with Image.open("Tests/images/ftex_uncompressed.ftu") as im:
|
with Image.open("Tests/images/ftex_uncompressed.ftu") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png")
|
assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png")
|
||||||
|
|
||||||
|
|
||||||
def test_load_dxt1():
|
def test_load_dxt1() -> None:
|
||||||
with Image.open("Tests/images/ftex_dxt1.ftc") as im:
|
with Image.open("Tests/images/ftex_dxt1.ftc") as im:
|
||||||
with Image.open("Tests/images/ftex_dxt1.png") as target:
|
with Image.open("Tests/images/ftex_dxt1.png") as target:
|
||||||
assert_image_similar(im, target.convert("RGBA"), 15)
|
assert_image_similar(im, target.convert("RGBA"), 15)
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import GbrImagePlugin, Image
|
from PIL import GbrImagePlugin, Image
|
||||||
|
@ -6,12 +7,12 @@ from PIL import GbrImagePlugin, Image
|
||||||
from .helper import assert_image_equal_tofile
|
from .helper import assert_image_equal_tofile
|
||||||
|
|
||||||
|
|
||||||
def test_gbr_file():
|
def test_gbr_file() -> None:
|
||||||
with Image.open("Tests/images/gbr.gbr") as im:
|
with Image.open("Tests/images/gbr.gbr") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
||||||
|
|
||||||
|
|
||||||
def test_load():
|
def test_load() -> None:
|
||||||
with Image.open("Tests/images/gbr.gbr") as im:
|
with Image.open("Tests/images/gbr.gbr") as im:
|
||||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||||
|
|
||||||
|
@ -19,14 +20,14 @@ def test_load():
|
||||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_load_operations():
|
def test_multiple_load_operations() -> None:
|
||||||
with Image.open("Tests/images/gbr.gbr") as im:
|
with Image.open("Tests/images/gbr.gbr") as im:
|
||||||
im.load()
|
im.load()
|
||||||
im.load()
|
im.load()
|
||||||
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import GdImageFile, UnidentifiedImageError
|
from PIL import GdImageFile, UnidentifiedImageError
|
||||||
|
@ -6,18 +7,18 @@ from PIL import GdImageFile, UnidentifiedImageError
|
||||||
TEST_GD_FILE = "Tests/images/hopper.gd"
|
TEST_GD_FILE = "Tests/images/hopper.gd"
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
with GdImageFile.open(TEST_GD_FILE) as im:
|
with GdImageFile.open(TEST_GD_FILE) as im:
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
assert im.format == "GD"
|
assert im.format == "GD"
|
||||||
|
|
||||||
|
|
||||||
def test_bad_mode():
|
def test_bad_mode() -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
GdImageFile.open(TEST_GD_FILE, "bad mode")
|
GdImageFile.open(TEST_GD_FILE, "bad mode")
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -22,7 +25,7 @@ with open(TEST_GIF, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
im.load()
|
im.load()
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
|
@ -32,8 +35,8 @@ def test_sanity():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||||
def test_unclosed_file():
|
def test_unclosed_file() -> None:
|
||||||
def open():
|
def open() -> None:
|
||||||
im = Image.open(TEST_GIF)
|
im = Image.open(TEST_GIF)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -41,14 +44,14 @@ def test_unclosed_file():
|
||||||
open()
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file() -> None:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
im = Image.open(TEST_GIF)
|
im = Image.open(TEST_GIF)
|
||||||
im.load()
|
im.load()
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
|
||||||
def test_seek_after_close():
|
def test_seek_after_close() -> None:
|
||||||
im = Image.open("Tests/images/iss634.gif")
|
im = Image.open("Tests/images/iss634.gif")
|
||||||
im.load()
|
im.load()
|
||||||
im.close()
|
im.close()
|
||||||
|
@ -61,20 +64,20 @@ def test_seek_after_close():
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
|
||||||
|
|
||||||
def test_context_manager():
|
def test_context_manager() -> None:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
GifImagePlugin.GifImageFile(invalid_file)
|
GifImagePlugin.GifImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
def test_l_mode_transparency():
|
def test_l_mode_transparency() -> None:
|
||||||
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
|
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
|
||||||
assert im.mode == "L"
|
assert im.mode == "L"
|
||||||
assert im.load()[0, 0] == 128
|
assert im.load()[0, 0] == 128
|
||||||
|
@ -85,7 +88,7 @@ def test_l_mode_transparency():
|
||||||
assert im.load()[0, 0] == 128
|
assert im.load()[0, 0] == 128
|
||||||
|
|
||||||
|
|
||||||
def test_l_mode_after_rgb():
|
def test_l_mode_after_rgb() -> None:
|
||||||
with Image.open("Tests/images/no_palette_after_rgb.gif") as im:
|
with Image.open("Tests/images/no_palette_after_rgb.gif") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
|
@ -94,13 +97,13 @@ def test_l_mode_after_rgb():
|
||||||
assert im.mode == "RGB"
|
assert im.mode == "RGB"
|
||||||
|
|
||||||
|
|
||||||
def test_palette_not_needed_for_second_frame():
|
def test_palette_not_needed_for_second_frame() -> None:
|
||||||
with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
|
with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert_image_similar(im, hopper("L").convert("RGB"), 8)
|
assert_image_similar(im, hopper("L").convert("RGB"), 8)
|
||||||
|
|
||||||
|
|
||||||
def test_strategy():
|
def test_strategy() -> None:
|
||||||
with Image.open("Tests/images/iss634.gif") as im:
|
with Image.open("Tests/images/iss634.gif") as im:
|
||||||
expected_rgb_always = im.convert("RGB")
|
expected_rgb_always = im.convert("RGB")
|
||||||
|
|
||||||
|
@ -141,14 +144,14 @@ def test_strategy():
|
||||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||||
|
|
||||||
|
|
||||||
def test_optimize():
|
def test_optimize() -> None:
|
||||||
def test_grayscale(optimize):
|
def test_grayscale(optimize: int) -> int:
|
||||||
im = Image.new("L", (1, 1), 0)
|
im = Image.new("L", (1, 1), 0)
|
||||||
filename = BytesIO()
|
filename = BytesIO()
|
||||||
im.save(filename, "GIF", optimize=optimize)
|
im.save(filename, "GIF", optimize=optimize)
|
||||||
return len(filename.getvalue())
|
return len(filename.getvalue())
|
||||||
|
|
||||||
def test_bilevel(optimize):
|
def test_bilevel(optimize: int) -> int:
|
||||||
im = Image.new("1", (1, 1), 0)
|
im = Image.new("1", (1, 1), 0)
|
||||||
test_file = BytesIO()
|
test_file = BytesIO()
|
||||||
im.save(test_file, "GIF", optimize=optimize)
|
im.save(test_file, "GIF", optimize=optimize)
|
||||||
|
@ -176,7 +179,9 @@ def test_optimize():
|
||||||
(4, 513, 256),
|
(4, 513, 256),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_optimize_correctness(colors, size, expected_palette_length):
|
def test_optimize_correctness(
|
||||||
|
colors: int, size: int, expected_palette_length: int
|
||||||
|
) -> None:
|
||||||
# 256 color Palette image, posterize to > 128 and < 128 levels.
|
# 256 color Palette image, posterize to > 128 and < 128 levels.
|
||||||
# Size bigger and smaller than 512x512.
|
# Size bigger and smaller than 512x512.
|
||||||
# Check the palette for number of colors allocated.
|
# Check the palette for number of colors allocated.
|
||||||
|
@ -198,14 +203,14 @@ def test_optimize_correctness(colors, size, expected_palette_length):
|
||||||
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
|
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_optimize_full_l():
|
def test_optimize_full_l() -> None:
|
||||||
im = Image.frombytes("L", (16, 16), bytes(range(256)))
|
im = Image.frombytes("L", (16, 16), bytes(range(256)))
|
||||||
test_file = BytesIO()
|
test_file = BytesIO()
|
||||||
im.save(test_file, "GIF", optimize=True)
|
im.save(test_file, "GIF", optimize=True)
|
||||||
assert im.mode == "L"
|
assert im.mode == "L"
|
||||||
|
|
||||||
|
|
||||||
def test_optimize_if_palette_can_be_reduced_by_half():
|
def test_optimize_if_palette_can_be_reduced_by_half() -> None:
|
||||||
im = Image.new("P", (8, 1))
|
im = Image.new("P", (8, 1))
|
||||||
im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
|
im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
|
||||||
for i in range(8):
|
for i in range(8):
|
||||||
|
@ -218,7 +223,7 @@ def test_optimize_if_palette_can_be_reduced_by_half():
|
||||||
assert len(reloaded.palette.palette) // 3 == colors
|
assert len(reloaded.palette.palette) // 3 == colors
|
||||||
|
|
||||||
|
|
||||||
def test_full_palette_second_frame(tmp_path):
|
def test_full_palette_second_frame(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = Image.new("P", (1, 256))
|
im = Image.new("P", (1, 256))
|
||||||
|
|
||||||
|
@ -239,7 +244,7 @@ def test_full_palette_second_frame(tmp_path):
|
||||||
reloaded.getpixel((0, i)) == i
|
reloaded.getpixel((0, i)) == i
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip(tmp_path):
|
def test_roundtrip(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
@ -247,7 +252,7 @@ def test_roundtrip(tmp_path):
|
||||||
assert_image_similar(reread.convert("RGB"), im, 50)
|
assert_image_similar(reread.convert("RGB"), im, 50)
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip2(tmp_path):
|
def test_roundtrip2(tmp_path: Path) -> None:
|
||||||
# see https://github.com/python-pillow/Pillow/issues/403
|
# see https://github.com/python-pillow/Pillow/issues/403
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
|
@ -257,7 +262,7 @@ def test_roundtrip2(tmp_path):
|
||||||
assert_image_similar(reread.convert("RGB"), hopper(), 50)
|
assert_image_similar(reread.convert("RGB"), hopper(), 50)
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip_save_all(tmp_path):
|
def test_roundtrip_save_all(tmp_path: Path) -> None:
|
||||||
# Single frame image
|
# Single frame image
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
@ -274,7 +279,7 @@ def test_roundtrip_save_all(tmp_path):
|
||||||
assert reread.n_frames == 5
|
assert reread.n_frames == 5
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip_save_all_1(tmp_path):
|
def test_roundtrip_save_all_1(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = Image.new("1", (1, 1))
|
im = Image.new("1", (1, 1))
|
||||||
im2 = Image.new("1", (1, 1), 1)
|
im2 = Image.new("1", (1, 1), 1)
|
||||||
|
@ -295,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path):
|
||||||
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
|
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_loading_multiple_palettes(path, mode):
|
def test_loading_multiple_palettes(path: str, mode: str) -> None:
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
first_frame_colors = im.palette.colors.keys()
|
first_frame_colors = im.palette.colors.keys()
|
||||||
|
@ -313,7 +318,7 @@ def test_loading_multiple_palettes(path, mode):
|
||||||
assert im.load()[24, 24] not in first_frame_colors
|
assert im.load()[24, 24] not in first_frame_colors
|
||||||
|
|
||||||
|
|
||||||
def test_headers_saving_for_animated_gifs(tmp_path):
|
def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
|
||||||
important_headers = ["background", "version", "duration", "loop"]
|
important_headers = ["background", "version", "duration", "loop"]
|
||||||
# Multiframe image
|
# Multiframe image
|
||||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||||
|
@ -326,7 +331,7 @@ def test_headers_saving_for_animated_gifs(tmp_path):
|
||||||
assert info[header] == reread.info[header]
|
assert info[header] == reread.info[header]
|
||||||
|
|
||||||
|
|
||||||
def test_palette_handling(tmp_path):
|
def test_palette_handling(tmp_path: Path) -> None:
|
||||||
# see https://github.com/python-pillow/Pillow/issues/513
|
# see https://github.com/python-pillow/Pillow/issues/513
|
||||||
|
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
|
@ -342,12 +347,12 @@ def test_palette_handling(tmp_path):
|
||||||
assert_image_similar(im, reloaded.convert("RGB"), 10)
|
assert_image_similar(im, reloaded.convert("RGB"), 10)
|
||||||
|
|
||||||
|
|
||||||
def test_palette_434(tmp_path):
|
def test_palette_434(tmp_path: Path) -> None:
|
||||||
# see https://github.com/python-pillow/Pillow/issues/434
|
# see https://github.com/python-pillow/Pillow/issues/434
|
||||||
|
|
||||||
def roundtrip(im, *args, **kwargs):
|
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im.copy().save(out, *args, **kwargs)
|
im.copy().save(out, **kwargs)
|
||||||
reloaded = Image.open(out)
|
reloaded = Image.open(out)
|
||||||
|
|
||||||
return reloaded
|
return reloaded
|
||||||
|
@ -367,7 +372,7 @@ def test_palette_434(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
||||||
def test_save_netpbm_bmp_mode(tmp_path):
|
def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_GIF) as img:
|
with Image.open(TEST_GIF) as img:
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
|
@ -378,7 +383,7 @@ def test_save_netpbm_bmp_mode(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
@pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available")
|
||||||
def test_save_netpbm_l_mode(tmp_path):
|
def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_GIF) as img:
|
with Image.open(TEST_GIF) as img:
|
||||||
img = img.convert("L")
|
img = img.convert("L")
|
||||||
|
|
||||||
|
@ -388,7 +393,7 @@ def test_save_netpbm_l_mode(tmp_path):
|
||||||
assert_image_similar(img, reloaded.convert("L"), 0)
|
assert_image_similar(img, reloaded.convert("L"), 0)
|
||||||
|
|
||||||
|
|
||||||
def test_seek():
|
def test_seek() -> None:
|
||||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
try:
|
try:
|
||||||
|
@ -399,7 +404,7 @@ def test_seek():
|
||||||
assert frame_count == 5
|
assert frame_count == 5
|
||||||
|
|
||||||
|
|
||||||
def test_seek_info():
|
def test_seek_info() -> None:
|
||||||
with Image.open("Tests/images/iss634.gif") as im:
|
with Image.open("Tests/images/iss634.gif") as im:
|
||||||
info = im.info.copy()
|
info = im.info.copy()
|
||||||
|
|
||||||
|
@ -409,7 +414,7 @@ def test_seek_info():
|
||||||
assert im.info == info
|
assert im.info == info
|
||||||
|
|
||||||
|
|
||||||
def test_seek_rewind():
|
def test_seek_rewind() -> None:
|
||||||
with Image.open("Tests/images/iss634.gif") as im:
|
with Image.open("Tests/images/iss634.gif") as im:
|
||||||
im.seek(2)
|
im.seek(2)
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
@ -427,7 +432,7 @@ def test_seek_rewind():
|
||||||
("Tests/images/iss634.gif", 42),
|
("Tests/images/iss634.gif", 42),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_n_frames(path, n_frames):
|
def test_n_frames(path: str, n_frames: int) -> None:
|
||||||
# Test is_animated before n_frames
|
# Test is_animated before n_frames
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
assert im.is_animated == (n_frames != 1)
|
assert im.is_animated == (n_frames != 1)
|
||||||
|
@ -438,7 +443,7 @@ def test_n_frames(path, n_frames):
|
||||||
assert im.is_animated == (n_frames != 1)
|
assert im.is_animated == (n_frames != 1)
|
||||||
|
|
||||||
|
|
||||||
def test_no_change():
|
def test_no_change() -> None:
|
||||||
# Test n_frames does not change the image
|
# Test n_frames does not change the image
|
||||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
@ -459,7 +464,7 @@ def test_no_change():
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror():
|
def test_eoferror() -> None:
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
|
@ -472,13 +477,13 @@ def test_eoferror():
|
||||||
im.seek(n_frames - 1)
|
im.seek(n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
def test_first_frame_transparency():
|
def test_first_frame_transparency() -> None:
|
||||||
with Image.open("Tests/images/first_frame_transparency.gif") as im:
|
with Image.open("Tests/images/first_frame_transparency.gif") as im:
|
||||||
px = im.load()
|
px = im.load()
|
||||||
assert px[0, 0] == im.info["transparency"]
|
assert px[0, 0] == im.info["transparency"]
|
||||||
|
|
||||||
|
|
||||||
def test_dispose_none():
|
def test_dispose_none() -> None:
|
||||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
@ -488,7 +493,7 @@ def test_dispose_none():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_dispose_none_load_end():
|
def test_dispose_none_load_end() -> None:
|
||||||
# Test image created with:
|
# Test image created with:
|
||||||
#
|
#
|
||||||
# im = Image.open("transparent.gif")
|
# im = Image.open("transparent.gif")
|
||||||
|
@ -501,7 +506,7 @@ def test_dispose_none_load_end():
|
||||||
assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
|
assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
|
||||||
|
|
||||||
|
|
||||||
def test_dispose_background():
|
def test_dispose_background() -> None:
|
||||||
with Image.open("Tests/images/dispose_bgnd.gif") as img:
|
with Image.open("Tests/images/dispose_bgnd.gif") as img:
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
@ -511,7 +516,7 @@ def test_dispose_background():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_dispose_background_transparency():
|
def test_dispose_background_transparency() -> None:
|
||||||
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
|
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
|
||||||
img.seek(2)
|
img.seek(2)
|
||||||
px = img.load()
|
px = img.load()
|
||||||
|
@ -539,7 +544,10 @@ def test_dispose_background_transparency():
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_transparent_dispose(loading_strategy, expected_colors):
|
def test_transparent_dispose(
|
||||||
|
loading_strategy: GifImagePlugin.LoadingStrategy,
|
||||||
|
expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],
|
||||||
|
) -> None:
|
||||||
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||||
try:
|
try:
|
||||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||||
|
@ -552,7 +560,7 @@ def test_transparent_dispose(loading_strategy, expected_colors):
|
||||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||||
|
|
||||||
|
|
||||||
def test_dispose_previous():
|
def test_dispose_previous() -> None:
|
||||||
with Image.open("Tests/images/dispose_prev.gif") as img:
|
with Image.open("Tests/images/dispose_prev.gif") as img:
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
@ -562,7 +570,7 @@ def test_dispose_previous():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_dispose_previous_first_frame():
|
def test_dispose_previous_first_frame() -> None:
|
||||||
with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
|
with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert_image_equal_tofile(
|
assert_image_equal_tofile(
|
||||||
|
@ -570,7 +578,7 @@ def test_dispose_previous_first_frame():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_previous_frame_loaded():
|
def test_previous_frame_loaded() -> None:
|
||||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||||
img.load()
|
img.load()
|
||||||
img.seek(1)
|
img.seek(1)
|
||||||
|
@ -581,7 +589,7 @@ def test_previous_frame_loaded():
|
||||||
assert_image_equal(img_skipped, img)
|
assert_image_equal(img_skipped, img)
|
||||||
|
|
||||||
|
|
||||||
def test_save_dispose(tmp_path):
|
def test_save_dispose(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im_list = [
|
im_list = [
|
||||||
Image.new("L", (100, 100), "#000"),
|
Image.new("L", (100, 100), "#000"),
|
||||||
|
@ -609,7 +617,7 @@ def test_save_dispose(tmp_path):
|
||||||
assert img.disposal_method == i + 1
|
assert img.disposal_method == i + 1
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_palette(tmp_path):
|
def test_dispose2_palette(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
# Four colors: white, gray, black, red
|
# Four colors: white, gray, black, red
|
||||||
|
@ -640,7 +648,7 @@ def test_dispose2_palette(tmp_path):
|
||||||
assert rgb_img.getpixel((50, 50)) == circle
|
assert rgb_img.getpixel((50, 50)) == circle
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_diff(tmp_path):
|
def test_dispose2_diff(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
# 4 frames: red/blue, red/red, blue/blue, red/blue
|
# 4 frames: red/blue, red/red, blue/blue, red/blue
|
||||||
|
@ -682,7 +690,7 @@ def test_dispose2_diff(tmp_path):
|
||||||
assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0)
|
assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_background(tmp_path):
|
def test_dispose2_background(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
im_list = []
|
im_list = []
|
||||||
|
@ -708,7 +716,7 @@ def test_dispose2_background(tmp_path):
|
||||||
assert im.getpixel((0, 0)) == (255, 0, 0)
|
assert im.getpixel((0, 0)) == (255, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_background_frame(tmp_path):
|
def test_dispose2_background_frame(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
im_list = [Image.new("RGBA", (1, 20))]
|
im_list = [Image.new("RGBA", (1, 20))]
|
||||||
|
@ -726,7 +734,7 @@ def test_dispose2_background_frame(tmp_path):
|
||||||
assert im.n_frames == 3
|
assert im.n_frames == 3
|
||||||
|
|
||||||
|
|
||||||
def test_transparency_in_second_frame(tmp_path):
|
def test_transparency_in_second_frame(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||||
assert im.info["transparency"] == 0
|
assert im.info["transparency"] == 0
|
||||||
|
@ -746,7 +754,7 @@ def test_transparency_in_second_frame(tmp_path):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_no_transparency_in_second_frame():
|
def test_no_transparency_in_second_frame() -> None:
|
||||||
with Image.open("Tests/images/iss634.gif") as img:
|
with Image.open("Tests/images/iss634.gif") as img:
|
||||||
# Seek to the second frame
|
# Seek to the second frame
|
||||||
img.seek(img.tell() + 1)
|
img.seek(img.tell() + 1)
|
||||||
|
@ -756,7 +764,7 @@ def test_no_transparency_in_second_frame():
|
||||||
assert img.histogram()[255] == 0
|
assert img.histogram()[255] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_remapped_transparency(tmp_path):
|
def test_remapped_transparency(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
im = Image.new("P", (1, 2))
|
im = Image.new("P", (1, 2))
|
||||||
|
@ -772,7 +780,7 @@ def test_remapped_transparency(tmp_path):
|
||||||
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
|
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
|
||||||
|
|
||||||
|
|
||||||
def test_duration(tmp_path):
|
def test_duration(tmp_path: Path) -> None:
|
||||||
duration = 1000
|
duration = 1000
|
||||||
|
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
@ -786,7 +794,7 @@ def test_duration(tmp_path):
|
||||||
assert reread.info["duration"] == duration
|
assert reread.info["duration"] == duration
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_duration(tmp_path):
|
def test_multiple_duration(tmp_path: Path) -> None:
|
||||||
duration_list = [1000, 2000, 3000]
|
duration_list = [1000, 2000, 3000]
|
||||||
|
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
@ -821,7 +829,7 @@ def test_multiple_duration(tmp_path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip_info_duration(tmp_path):
|
def test_roundtrip_info_duration(tmp_path: Path) -> None:
|
||||||
duration_list = [100, 500, 500]
|
duration_list = [100, 500, 500]
|
||||||
|
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
@ -838,7 +846,7 @@ def test_roundtrip_info_duration(tmp_path):
|
||||||
] == duration_list
|
] == duration_list
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip_info_duration_combined(tmp_path):
|
def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
with Image.open("Tests/images/duplicate_frame.gif") as im:
|
with Image.open("Tests/images/duplicate_frame.gif") as im:
|
||||||
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
|
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
|
||||||
|
@ -854,7 +862,7 @@ def test_roundtrip_info_duration_combined(tmp_path):
|
||||||
] == [1000, 2000]
|
] == [1000, 2000]
|
||||||
|
|
||||||
|
|
||||||
def test_identical_frames(tmp_path):
|
def test_identical_frames(tmp_path: Path) -> None:
|
||||||
duration_list = [1000, 1500, 2000, 4000]
|
duration_list = [1000, 1500, 2000, 4000]
|
||||||
|
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
@ -887,7 +895,9 @@ def test_identical_frames(tmp_path):
|
||||||
1500,
|
1500,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_identical_frames_to_single_frame(duration, tmp_path):
|
def test_identical_frames_to_single_frame(
|
||||||
|
duration: int | list[int], tmp_path: Path
|
||||||
|
) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im_list = [
|
im_list = [
|
||||||
Image.new("L", (100, 100), "#000"),
|
Image.new("L", (100, 100), "#000"),
|
||||||
|
@ -904,7 +914,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
|
||||||
assert reread.info["duration"] == 4500
|
assert reread.info["duration"] == 4500
|
||||||
|
|
||||||
|
|
||||||
def test_loop_none(tmp_path):
|
def test_loop_none(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = Image.new("L", (100, 100), "#000")
|
im = Image.new("L", (100, 100), "#000")
|
||||||
im.save(out, loop=None)
|
im.save(out, loop=None)
|
||||||
|
@ -912,7 +922,7 @@ def test_loop_none(tmp_path):
|
||||||
assert "loop" not in reread.info
|
assert "loop" not in reread.info
|
||||||
|
|
||||||
|
|
||||||
def test_number_of_loops(tmp_path):
|
def test_number_of_loops(tmp_path: Path) -> None:
|
||||||
number_of_loops = 2
|
number_of_loops = 2
|
||||||
|
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
@ -930,7 +940,7 @@ def test_number_of_loops(tmp_path):
|
||||||
assert im.info["loop"] == 2
|
assert im.info["loop"] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_background(tmp_path):
|
def test_background(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = Image.new("L", (100, 100), "#000")
|
im = Image.new("L", (100, 100), "#000")
|
||||||
im.info["background"] = 1
|
im.info["background"] = 1
|
||||||
|
@ -939,7 +949,7 @@ def test_background(tmp_path):
|
||||||
assert reread.info["background"] == im.info["background"]
|
assert reread.info["background"] == im.info["background"]
|
||||||
|
|
||||||
|
|
||||||
def test_webp_background(tmp_path):
|
def test_webp_background(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
# Test opaque WebP background
|
# Test opaque WebP background
|
||||||
|
@ -954,7 +964,7 @@ def test_webp_background(tmp_path):
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
||||||
|
|
||||||
def test_comment(tmp_path):
|
def test_comment(tmp_path: Path) -> None:
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
|
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0"
|
||||||
|
|
||||||
|
@ -974,7 +984,7 @@ def test_comment(tmp_path):
|
||||||
assert reread.info["version"] == b"GIF89a"
|
assert reread.info["version"] == b"GIF89a"
|
||||||
|
|
||||||
|
|
||||||
def test_comment_over_255(tmp_path):
|
def test_comment_over_255(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = Image.new("L", (100, 100), "#000")
|
im = Image.new("L", (100, 100), "#000")
|
||||||
comment = b"Test comment text"
|
comment = b"Test comment text"
|
||||||
|
@ -989,18 +999,18 @@ def test_comment_over_255(tmp_path):
|
||||||
assert reread.info["version"] == b"GIF89a"
|
assert reread.info["version"] == b"GIF89a"
|
||||||
|
|
||||||
|
|
||||||
def test_zero_comment_subblocks():
|
def test_zero_comment_subblocks() -> None:
|
||||||
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
|
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
|
||||||
assert_image_equal_tofile(im, TEST_GIF)
|
assert_image_equal_tofile(im, TEST_GIF)
|
||||||
|
|
||||||
|
|
||||||
def test_read_multiple_comment_blocks():
|
def test_read_multiple_comment_blocks() -> None:
|
||||||
with Image.open("Tests/images/multiple_comments.gif") as im:
|
with Image.open("Tests/images/multiple_comments.gif") as im:
|
||||||
# Multiple comment blocks in a frame are separated not concatenated
|
# Multiple comment blocks in a frame are separated not concatenated
|
||||||
assert im.info["comment"] == b"Test comment 1\nTest comment 2"
|
assert im.info["comment"] == b"Test comment 1\nTest comment 2"
|
||||||
|
|
||||||
|
|
||||||
def test_empty_string_comment(tmp_path):
|
def test_empty_string_comment(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
with Image.open("Tests/images/chi.gif") as im:
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
assert "comment" in im.info
|
assert "comment" in im.info
|
||||||
|
@ -1013,7 +1023,7 @@ def test_empty_string_comment(tmp_path):
|
||||||
assert "comment" not in frame.info
|
assert "comment" not in frame.info
|
||||||
|
|
||||||
|
|
||||||
def test_retain_comment_in_subsequent_frames(tmp_path):
|
def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
|
||||||
# Test that a comment block at the beginning is kept
|
# Test that a comment block at the beginning is kept
|
||||||
with Image.open("Tests/images/chi.gif") as im:
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
for frame in ImageSequence.Iterator(im):
|
for frame in ImageSequence.Iterator(im):
|
||||||
|
@ -1044,10 +1054,10 @@ def test_retain_comment_in_subsequent_frames(tmp_path):
|
||||||
assert frame.info["comment"] == b"Test"
|
assert frame.info["comment"] == b"Test"
|
||||||
|
|
||||||
|
|
||||||
def test_version(tmp_path):
|
def test_version(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
def assert_version_after_save(im, version):
|
def assert_version_after_save(im: Image.Image, version: bytes) -> None:
|
||||||
im.save(out)
|
im.save(out)
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
assert reread.info["version"] == version
|
assert reread.info["version"] == version
|
||||||
|
@ -1074,7 +1084,7 @@ def test_version(tmp_path):
|
||||||
assert_version_after_save(im, b"GIF87a")
|
assert_version_after_save(im, b"GIF87a")
|
||||||
|
|
||||||
|
|
||||||
def test_append_images(tmp_path):
|
def test_append_images(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
# Test appending single frame images
|
# Test appending single frame images
|
||||||
|
@ -1086,7 +1096,7 @@ def test_append_images(tmp_path):
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Tests appending using a generator
|
# Tests appending using a generator
|
||||||
def im_generator(ims):
|
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||||
yield from ims
|
yield from ims
|
||||||
|
|
||||||
im.save(out, save_all=True, append_images=im_generator(ims))
|
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||||
|
@ -1103,7 +1113,7 @@ def test_append_images(tmp_path):
|
||||||
assert reread.n_frames == 10
|
assert reread.n_frames == 10
|
||||||
|
|
||||||
|
|
||||||
def test_transparent_optimize(tmp_path):
|
def test_transparent_optimize(tmp_path: Path) -> None:
|
||||||
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
||||||
# transparency.
|
# transparency.
|
||||||
# Need a palette that isn't using the 0 color,
|
# Need a palette that isn't using the 0 color,
|
||||||
|
@ -1123,7 +1133,7 @@ def test_transparent_optimize(tmp_path):
|
||||||
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
|
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
|
||||||
|
|
||||||
|
|
||||||
def test_removed_transparency(tmp_path):
|
def test_removed_transparency(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = Image.new("RGB", (256, 1))
|
im = Image.new("RGB", (256, 1))
|
||||||
|
|
||||||
|
@ -1138,7 +1148,7 @@ def test_removed_transparency(tmp_path):
|
||||||
assert "transparency" not in reloaded.info
|
assert "transparency" not in reloaded.info
|
||||||
|
|
||||||
|
|
||||||
def test_rgb_transparency(tmp_path):
|
def test_rgb_transparency(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
# Single frame
|
# Single frame
|
||||||
|
@ -1160,7 +1170,7 @@ def test_rgb_transparency(tmp_path):
|
||||||
assert "transparency" not in reloaded.info
|
assert "transparency" not in reloaded.info
|
||||||
|
|
||||||
|
|
||||||
def test_rgba_transparency(tmp_path):
|
def test_rgba_transparency(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
|
@ -1171,13 +1181,13 @@ def test_rgba_transparency(tmp_path):
|
||||||
assert_image_equal(hopper("P").convert("RGB"), reloaded)
|
assert_image_equal(hopper("P").convert("RGB"), reloaded)
|
||||||
|
|
||||||
|
|
||||||
def test_background_outside_palettte(tmp_path):
|
def test_background_outside_palettte(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/background_outside_palette.gif") as im:
|
with Image.open("Tests/images/background_outside_palette.gif") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
assert im.info["background"] == 255
|
assert im.info["background"] == 255
|
||||||
|
|
||||||
|
|
||||||
def test_bbox(tmp_path):
|
def test_bbox(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
im = Image.new("RGB", (100, 100), "#fff")
|
im = Image.new("RGB", (100, 100), "#fff")
|
||||||
|
@ -1188,7 +1198,7 @@ def test_bbox(tmp_path):
|
||||||
assert reread.n_frames == 2
|
assert reread.n_frames == 2
|
||||||
|
|
||||||
|
|
||||||
def test_bbox_alpha(tmp_path):
|
def test_bbox_alpha(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
|
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
|
||||||
|
@ -1200,7 +1210,7 @@ def test_bbox_alpha(tmp_path):
|
||||||
assert reread.n_frames == 2
|
assert reread.n_frames == 2
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_L(tmp_path):
|
def test_palette_save_L(tmp_path: Path) -> None:
|
||||||
# Generate an L mode image with a separate palette
|
# Generate an L mode image with a separate palette
|
||||||
|
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
|
@ -1214,7 +1224,7 @@ def test_palette_save_L(tmp_path):
|
||||||
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_P(tmp_path):
|
def test_palette_save_P(tmp_path: Path) -> None:
|
||||||
im = Image.new("P", (1, 2))
|
im = Image.new("P", (1, 2))
|
||||||
im.putpixel((0, 1), 1)
|
im.putpixel((0, 1), 1)
|
||||||
|
|
||||||
|
@ -1228,7 +1238,7 @@ def test_palette_save_P(tmp_path):
|
||||||
assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
|
assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_duplicate_entries(tmp_path):
|
def test_palette_save_duplicate_entries(tmp_path: Path) -> None:
|
||||||
im = Image.new("P", (1, 2))
|
im = Image.new("P", (1, 2))
|
||||||
im.putpixel((0, 1), 1)
|
im.putpixel((0, 1), 1)
|
||||||
|
|
||||||
|
@ -1241,7 +1251,7 @@ def test_palette_save_duplicate_entries(tmp_path):
|
||||||
assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0)
|
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: Path) -> None:
|
||||||
frames = []
|
frames = []
|
||||||
colors = ((255, 0, 0), (0, 255, 0))
|
colors = ((255, 0, 0), (0, 255, 0))
|
||||||
for color in colors:
|
for color in colors:
|
||||||
|
@ -1264,7 +1274,7 @@ def test_palette_save_all_P(tmp_path):
|
||||||
assert im.palette.palette == im.global_palette.palette
|
assert im.palette.palette == im.global_palette.palette
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_ImagePalette(tmp_path):
|
def test_palette_save_ImagePalette(tmp_path: Path) -> None:
|
||||||
# Pass in a different palette, as an ImagePalette.ImagePalette
|
# Pass in a different palette, as an ImagePalette.ImagePalette
|
||||||
# effectively the same as test_palette_save_P
|
# effectively the same as test_palette_save_P
|
||||||
|
|
||||||
|
@ -1279,7 +1289,7 @@ def test_palette_save_ImagePalette(tmp_path):
|
||||||
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
||||||
|
|
||||||
|
|
||||||
def test_save_I(tmp_path):
|
def test_save_I(tmp_path: Path) -> None:
|
||||||
# Test saving something that would trigger the auto-convert to 'L'
|
# Test saving something that would trigger the auto-convert to 'L'
|
||||||
|
|
||||||
im = hopper("I")
|
im = hopper("I")
|
||||||
|
@ -1291,7 +1301,7 @@ def test_save_I(tmp_path):
|
||||||
assert_image_equal(reloaded.convert("L"), im.convert("L"))
|
assert_image_equal(reloaded.convert("L"), im.convert("L"))
|
||||||
|
|
||||||
|
|
||||||
def test_getdata():
|
def test_getdata() -> None:
|
||||||
# Test getheader/getdata against legacy values.
|
# Test getheader/getdata against legacy values.
|
||||||
# Create a 'P' image with holes in the palette.
|
# Create a 'P' image with holes in the palette.
|
||||||
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
|
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
|
||||||
|
@ -1319,7 +1329,7 @@ def test_getdata():
|
||||||
GifImagePlugin._FORCE_OPTIMIZE = False
|
GifImagePlugin._FORCE_OPTIMIZE = False
|
||||||
|
|
||||||
|
|
||||||
def test_lzw_bits():
|
def test_lzw_bits() -> None:
|
||||||
# see https://github.com/python-pillow/Pillow/issues/2811
|
# see https://github.com/python-pillow/Pillow/issues/2811
|
||||||
with Image.open("Tests/images/issue_2811.gif") as im:
|
with Image.open("Tests/images/issue_2811.gif") as im:
|
||||||
assert im.tile[0][3][0] == 11 # LZW bits
|
assert im.tile[0][3][0] == 11 # LZW bits
|
||||||
|
@ -1327,7 +1337,7 @@ def test_lzw_bits():
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
def test_extents():
|
def test_extents() -> None:
|
||||||
with Image.open("Tests/images/test_extents.gif") as im:
|
with Image.open("Tests/images/test_extents.gif") as im:
|
||||||
assert im.size == (100, 100)
|
assert im.size == (100, 100)
|
||||||
|
|
||||||
|
@ -1339,7 +1349,7 @@ def test_extents():
|
||||||
assert im.size == (150, 150)
|
assert im.size == (150, 150)
|
||||||
|
|
||||||
|
|
||||||
def test_missing_background():
|
def test_missing_background() -> None:
|
||||||
# The Global Color Table Flag isn't set, so there is no background color index,
|
# The Global Color Table Flag isn't set, so there is no background color index,
|
||||||
# but the disposal method is "Restore to background color"
|
# but the disposal method is "Restore to background color"
|
||||||
with Image.open("Tests/images/missing_background.gif") as im:
|
with Image.open("Tests/images/missing_background.gif") as im:
|
||||||
|
@ -1347,7 +1357,7 @@ def test_missing_background():
|
||||||
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")
|
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")
|
||||||
|
|
||||||
|
|
||||||
def test_saving_rgba(tmp_path):
|
def test_saving_rgba(tmp_path: Path) -> None:
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
with Image.open("Tests/images/transparent.png") as im:
|
with Image.open("Tests/images/transparent.png") as im:
|
||||||
im.save(out)
|
im.save(out)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PIL import GimpGradientFile, ImagePalette
|
from PIL import GimpGradientFile, ImagePalette
|
||||||
|
|
||||||
|
|
||||||
def test_linear_pos_le_middle():
|
def test_linear_pos_le_middle() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 0.5
|
middle = 0.5
|
||||||
pos = 0.25
|
pos = 0.25
|
||||||
|
@ -14,7 +15,7 @@ def test_linear_pos_le_middle():
|
||||||
assert ret == 0.25
|
assert ret == 0.25
|
||||||
|
|
||||||
|
|
||||||
def test_linear_pos_le_small_middle():
|
def test_linear_pos_le_small_middle() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 1e-11
|
middle = 1e-11
|
||||||
pos = 1e-12
|
pos = 1e-12
|
||||||
|
@ -26,7 +27,7 @@ def test_linear_pos_le_small_middle():
|
||||||
assert ret == 0.0
|
assert ret == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_linear_pos_gt_middle():
|
def test_linear_pos_gt_middle() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 0.5
|
middle = 0.5
|
||||||
pos = 0.75
|
pos = 0.75
|
||||||
|
@ -38,7 +39,7 @@ def test_linear_pos_gt_middle():
|
||||||
assert ret == 0.75
|
assert ret == 0.75
|
||||||
|
|
||||||
|
|
||||||
def test_linear_pos_gt_small_middle():
|
def test_linear_pos_gt_small_middle() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 1 - 1e-11
|
middle = 1 - 1e-11
|
||||||
pos = 1 - 1e-12
|
pos = 1 - 1e-12
|
||||||
|
@ -50,7 +51,7 @@ def test_linear_pos_gt_small_middle():
|
||||||
assert ret == 1.0
|
assert ret == 1.0
|
||||||
|
|
||||||
|
|
||||||
def test_curved():
|
def test_curved() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 0.5
|
middle = 0.5
|
||||||
pos = 0.75
|
pos = 0.75
|
||||||
|
@ -62,7 +63,7 @@ def test_curved():
|
||||||
assert ret == 0.75
|
assert ret == 0.75
|
||||||
|
|
||||||
|
|
||||||
def test_sine():
|
def test_sine() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 0.5
|
middle = 0.5
|
||||||
pos = 0.75
|
pos = 0.75
|
||||||
|
@ -74,7 +75,7 @@ def test_sine():
|
||||||
assert ret == 0.8535533905932737
|
assert ret == 0.8535533905932737
|
||||||
|
|
||||||
|
|
||||||
def test_sphere_increasing():
|
def test_sphere_increasing() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 0.5
|
middle = 0.5
|
||||||
pos = 0.75
|
pos = 0.75
|
||||||
|
@ -86,7 +87,7 @@ def test_sphere_increasing():
|
||||||
assert round(abs(ret - 0.9682458365518543), 7) == 0
|
assert round(abs(ret - 0.9682458365518543), 7) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_sphere_decreasing():
|
def test_sphere_decreasing() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
middle = 0.5
|
middle = 0.5
|
||||||
pos = 0.75
|
pos = 0.75
|
||||||
|
@ -98,7 +99,7 @@ def test_sphere_decreasing():
|
||||||
assert ret == 0.3385621722338523
|
assert ret == 0.3385621722338523
|
||||||
|
|
||||||
|
|
||||||
def test_load_via_imagepalette():
|
def test_load_via_imagepalette() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
test_file = "Tests/images/gimp_gradient.ggr"
|
test_file = "Tests/images/gimp_gradient.ggr"
|
||||||
|
|
||||||
|
@ -111,7 +112,7 @@ def test_load_via_imagepalette():
|
||||||
assert palette[1] == "RGBA"
|
assert palette[1] == "RGBA"
|
||||||
|
|
||||||
|
|
||||||
def test_load_1_3_via_imagepalette():
|
def test_load_1_3_via_imagepalette() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
# GIMP 1.3 gradient files contain a name field
|
# GIMP 1.3 gradient files contain a name field
|
||||||
test_file = "Tests/images/gimp_gradient_with_name.ggr"
|
test_file = "Tests/images/gimp_gradient_with_name.ggr"
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL.GimpPaletteFile import GimpPaletteFile
|
from PIL.GimpPaletteFile import GimpPaletteFile
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity() -> None:
|
||||||
with open("Tests/images/test.gpl", "rb") as fp:
|
with open("Tests/images/test.gpl", "rb") as fp:
|
||||||
GimpPaletteFile(fp)
|
GimpPaletteFile(fp)
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ def test_sanity():
|
||||||
GimpPaletteFile(fp)
|
GimpPaletteFile(fp)
|
||||||
|
|
||||||
|
|
||||||
def test_get_palette():
|
def test_get_palette() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
|
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
|
||||||
palette_file = GimpPaletteFile(fp)
|
palette_file = GimpPaletteFile(fp)
|
||||||
|
|