Merge branch 'main' into master
|
@ -10,29 +10,29 @@ environment:
|
|||
TEST_OPTIONS:
|
||||
DEPLOY: YES
|
||||
matrix:
|
||||
- PYTHON: C:/Python39
|
||||
- PYTHON: C:/Python310
|
||||
ARCHITECTURE: x86
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
- PYTHON: C:/Python36-x64
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||
- PYTHON: C:/Python37-x64
|
||||
ARCHITECTURE: x64
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
|
||||
|
||||
install:
|
||||
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip
|
||||
- '%PYTHON%\%EXECUTABLE% --version'
|
||||
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||
- 7z x pillow-depends.zip -oc:\
|
||||
- mv c:\pillow-depends-master c:\pillow-depends
|
||||
- mv c:\pillow-depends-main c:\pillow-depends
|
||||
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
|
||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
||||
- ..\pillow-depends\gs9540w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.54.0\bin;%PATH%
|
||||
- ..\pillow-depends\gs9561w32.exe /S
|
||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
c:\pillow\winbuild\build\build_dep_all.cmd
|
||||
$host.SetShouldExit(0)
|
||||
- path C:\pillow\winbuild\build\bin;%PATH%
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install -U setuptools'
|
||||
|
||||
build_script:
|
||||
- ps: |
|
||||
|
@ -43,7 +43,7 @@ build_script:
|
|||
|
||||
test_script:
|
||||
- cd c:\pillow
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
|
||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
|
||||
|
@ -84,7 +84,7 @@ deploy:
|
|||
artifact: /.*egg|wheel/
|
||||
on:
|
||||
APPVEYOR_REPO_NAME: python-pillow/Pillow
|
||||
branch: master
|
||||
branch: main
|
||||
deploy: YES
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
# gather the coverage data
|
||||
pip3 install codecov
|
||||
python3 -m pip install codecov
|
||||
if [[ $MATRIX_DOCKER ]]; then
|
||||
coverage xml --ignore-errors
|
||||
else
|
||||
|
|
|
@ -19,7 +19,7 @@ set -e
|
|||
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake imagemagick libharfbuzz-dev libfribidi-dev
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade wheel
|
||||
|
@ -32,8 +32,7 @@ python3 -m pip install -U pytest-cov
|
|||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
python3 -m pip install test-image-results
|
||||
# TODO Remove condition when numpy supports 3.10
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# PyQt5 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
|
|
10
.github/CONTRIBUTING.md
vendored
|
@ -4,13 +4,13 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
|
|||
|
||||
## Bug fixes, feature additions, etc.
|
||||
|
||||
Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
|
||||
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
|
||||
|
||||
- Fork the Pillow repository.
|
||||
- Create a branch from master.
|
||||
- Create a branch from `main`.
|
||||
- Develop bug fixes, features, tests, etc.
|
||||
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow master.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow `main`.
|
||||
|
||||
### Guidelines
|
||||
|
||||
|
@ -18,7 +18,7 @@ Please send a pull request to the master branch. Please include [documentation](
|
|||
- Provide tests for any newly added code.
|
||||
- Follow PEP 8.
|
||||
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
|
||||
- Include [release notes](https://github.com/python-pillow/Pillow/tree/master/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
|
||||
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
|
@ -35,4 +35,4 @@ The best reproductions are self-contained scripts with minimal dependencies. If
|
|||
|
||||
## Security vulnerabilities
|
||||
|
||||
Please see our [security policy](https://github.com/python-pillow/Pillow/blob/master/.github/SECURITY.md).
|
||||
Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md).
|
||||
|
|
1
.github/mergify.yml
vendored
|
@ -7,6 +7,7 @@ pull_request_rules:
|
|||
- status-success=Test Successful
|
||||
- status-success=Docker Test Successful
|
||||
- status-success=Windows Test Successful
|
||||
- status-success=MinGW Test Successful
|
||||
- status-success=continuous-integration/appveyor/pr
|
||||
actions:
|
||||
merge:
|
||||
|
|
6
.github/workflows/cifuzz.yml
vendored
|
@ -1,4 +1,5 @@
|
|||
name: CIFuzz
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
|
@ -8,6 +9,7 @@ on:
|
|||
paths:
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
|
@ -29,13 +31,13 @@ jobs:
|
|||
language: python
|
||||
dry-run: false
|
||||
- name: Upload New Crash
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
path: ./out/artifacts
|
||||
- name: Upload Legacy Crash
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: steps.run.outcome == 'success'
|
||||
with:
|
||||
name: crash
|
||||
|
|
19
.github/workflows/lint.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -10,15 +10,7 @@ jobs:
|
|||
name: Lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: lint-pip-${{ hashFiles('**/setup.py') }}
|
||||
restore-keys: |
|
||||
lint-pip-
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: pre-commit cache
|
||||
uses: actions/cache@v2
|
||||
|
@ -29,9 +21,11 @@ jobs:
|
|||
lint-pre-commit-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
cache-dependency-path: "setup.py"
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
@ -45,4 +39,3 @@ jobs:
|
|||
run: tox -e lint
|
||||
env:
|
||||
PRE_COMMIT_COLOR: always
|
||||
|
||||
|
|
5
.github/workflows/release-drafter.yml
vendored
|
@ -4,14 +4,15 @@ on:
|
|||
push:
|
||||
# branches to consider in the event; optional, defaults to all
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
if: github.repository == 'python-pillow/Pillow'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Drafts your next release notes as pull requests are merged into "master"
|
||||
# Drafts your next release notes as pull requests are merged into "main"
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
27
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Close stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "10 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository_owner == 'python-pillow'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Check issues"
|
||||
uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "Awaiting OP Action"
|
||||
close-issue-message: "Closing this issue as no feedback has been received."
|
||||
days-before-stale: 7
|
||||
days-before-issue-close: 0
|
||||
days-before-pr-close: -1
|
||||
labels-to-remove-when-unstale: "Awaiting OP Action"
|
14
.github/workflows/test-docker.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Test Docker
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -19,14 +19,16 @@ jobs:
|
|||
amazon-2-amd64,
|
||||
arch,
|
||||
centos-7-amd64,
|
||||
centos-8-amd64,
|
||||
centos-stream-8-amd64,
|
||||
centos-stream-9-amd64,
|
||||
debian-10-buster-x86,
|
||||
fedora-33-amd64,
|
||||
fedora-34-amd64,
|
||||
debian-11-bullseye-x86,
|
||||
fedora-35-amd64,
|
||||
gentoo,
|
||||
ubuntu-18.04-bionic-amd64,
|
||||
ubuntu-20.04-focal-amd64,
|
||||
]
|
||||
dockerTag: [master]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-20.04-focal-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
|
@ -38,7 +40,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
85
.github/workflows/test-mingw.yml
vendored
Normal file
|
@ -0,0 +1,85 @@
|
|||
name: Test MinGW
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
mingw: ["MINGW32", "MINGW64"]
|
||||
include:
|
||||
- mingw: "MINGW32"
|
||||
name: "MSYS2 MinGW 32-bit"
|
||||
package: "mingw-w64-i686"
|
||||
- mingw: "MINGW64"
|
||||
name: "MSYS2 MinGW 64-bit"
|
||||
package: "mingw-w64-x86_64"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash.exe --login -eo pipefail "{0}"
|
||||
env:
|
||||
MSYSTEM: ${{ matrix.mingw }}
|
||||
CHERE_INVOKING: 1
|
||||
|
||||
timeout-minutes: 30
|
||||
name: ${{ matrix.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up shell
|
||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||
shell: pwsh
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pacman -S --noconfirm \
|
||||
${{ matrix.package }}-python3-cffi \
|
||||
${{ matrix.package }}-python3-numpy \
|
||||
${{ matrix.package }}-python3-olefile \
|
||||
${{ matrix.package }}-python3-pip \
|
||||
${{ matrix.package }}-python-pyqt6 \
|
||||
${{ matrix.package }}-python3-setuptools \
|
||||
${{ matrix.package }}-freetype \
|
||||
${{ matrix.package }}-gcc \
|
||||
${{ matrix.package }}-ghostscript \
|
||||
${{ matrix.package }}-lcms2 \
|
||||
${{ matrix.package }}-libimagequant \
|
||||
${{ matrix.package }}-libjpeg-turbo \
|
||||
${{ matrix.package }}-libraqm \
|
||||
${{ matrix.package }}-libtiff \
|
||||
${{ matrix.package }}-libwebp \
|
||||
${{ matrix.package }}-openjpeg2 \
|
||||
subversion
|
||||
|
||||
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
||||
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
||||
- name: Build Pillow
|
||||
run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" .
|
||||
|
||||
- name: Test Pillow
|
||||
run: |
|
||||
python3 selftest.py --installed
|
||||
python3 -c "from PIL import Image"
|
||||
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
|
||||
|
||||
- name: Upload coverage
|
||||
run: |
|
||||
python3 -m pip install codecov
|
||||
bash <(curl -s https://codecov.io/bash) -F GHA_Windows
|
||||
env:
|
||||
CODECOV_NAME: ${{ matrix.name }}
|
||||
|
||||
success:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: MinGW Test Successful
|
||||
steps:
|
||||
- name: Success
|
||||
run: echo MinGW Test Successful
|
13
.github/workflows/test-valgrind.yml
vendored
|
@ -11,6 +11,7 @@ on:
|
|||
paths:
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -22,12 +23,12 @@ jobs:
|
|||
docker: [
|
||||
ubuntu-20.04-focal-amd64-valgrind,
|
||||
]
|
||||
dockerTag: [master]
|
||||
dockerTag: [main]
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
@ -42,11 +43,3 @@ jobs:
|
|||
sudo chown -R 1000 $GITHUB_WORKSPACE
|
||||
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
sudo chown -R runner $GITHUB_WORKSPACE
|
||||
|
||||
success:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Valgrind Test Successful
|
||||
steps:
|
||||
- name: Success
|
||||
run: echo Valgrind Test Successful
|
||||
|
|
123
.github/workflows/test-windows.yml
vendored
|
@ -1,52 +1,44 @@
|
|||
name: Test Windows
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
architecture: ["x86", "x64"]
|
||||
include:
|
||||
# PyPy3.6 only ships 32-bit binaries for Windows
|
||||
- python-version: "pypy-3.6"
|
||||
architecture: "x86"
|
||||
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
|
||||
- python-version: "pypy-3.7"
|
||||
architecture: "x64"
|
||||
- python-version: "pypy-3.8"
|
||||
architecture: "x64"
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
name: Python ${{ matrix.python-version }} ${{ matrix.architecture }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key:
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-
|
||||
${{ runner.os }}-${{ matrix.python-version }}-
|
||||
|
||||
# sets env: pythonLocation
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
- name: Print build system information
|
||||
run: python .github/workflows/system-info.py
|
||||
|
@ -60,8 +52,8 @@ jobs:
|
|||
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
|
||||
|
||||
winbuild\depends\gs9540w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.54.0\bin" >> $env:GITHUB_PATH
|
||||
winbuild\depends\gs9561w32.exe /S
|
||||
echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
|
||||
|
||||
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
|
||||
|
||||
|
@ -140,15 +132,16 @@ jobs:
|
|||
- name: Build Pillow
|
||||
run: |
|
||||
$FLAGS=""
|
||||
if ('${{ github.event_name }}' -eq 'push') { $FLAGS="--disable-imagequant" }
|
||||
if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" }
|
||||
& winbuild\build\build_pillow.cmd $FLAGS install
|
||||
& $env:pythonLocation\python.exe selftest.py --installed
|
||||
shell: pwsh
|
||||
|
||||
# failing with PyPy3
|
||||
# skip PyPy for speed
|
||||
- name: Enable heap verification
|
||||
if: "!contains(matrix.python-version, 'pypy')"
|
||||
run: "& 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\gflags.exe' /p /enable $env:pythonLocation\\python.exe"
|
||||
run: |
|
||||
& reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
|
||||
|
||||
- name: Test Pillow
|
||||
run: |
|
||||
|
@ -163,7 +156,7 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
|
@ -183,92 +176,20 @@ jobs:
|
|||
|
||||
- name: Build wheel
|
||||
id: wheel
|
||||
if: "github.event_name == 'push'"
|
||||
if: "github.event_name != 'pull_request'"
|
||||
run: |
|
||||
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a
|
||||
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
|
||||
shell: cmd
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: "github.event_name == 'push'"
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: "github.event_name != 'pull_request'"
|
||||
with:
|
||||
name: ${{ steps.wheel.outputs.dist }}
|
||||
path: dist\*.whl
|
||||
|
||||
msys:
|
||||
runs-on: windows-2019
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
mingw: ["MINGW32", "MINGW64"]
|
||||
include:
|
||||
- mingw: "MINGW32"
|
||||
name: "MSYS2 MinGW 32-bit"
|
||||
package: "mingw-w64-i686"
|
||||
- mingw: "MINGW64"
|
||||
name: "MSYS2 MinGW 64-bit"
|
||||
package: "mingw-w64-x86_64"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash.exe --login -eo pipefail "{0}"
|
||||
env:
|
||||
MSYSTEM: ${{ matrix.mingw }}
|
||||
CHERE_INVOKING: 1
|
||||
|
||||
timeout-minutes: 30
|
||||
name: ${{ matrix.name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up shell
|
||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||
shell: pwsh
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pacman -S --noconfirm \
|
||||
${{ matrix.package }}-python3-cffi \
|
||||
${{ matrix.package }}-python3-numpy \
|
||||
${{ matrix.package }}-python3-olefile \
|
||||
${{ matrix.package }}-python3-pip \
|
||||
${{ matrix.package }}-python3-pyqt5 \
|
||||
${{ matrix.package }}-python3-setuptools \
|
||||
${{ matrix.package }}-freetype \
|
||||
${{ matrix.package }}-ghostscript \
|
||||
${{ matrix.package }}-lcms2 \
|
||||
${{ matrix.package }}-libimagequant \
|
||||
${{ matrix.package }}-libjpeg-turbo \
|
||||
${{ matrix.package }}-libraqm \
|
||||
${{ matrix.package }}-libtiff \
|
||||
${{ matrix.package }}-libwebp \
|
||||
${{ matrix.package }}-openjpeg2 \
|
||||
subversion
|
||||
|
||||
python3 -m pip install pyroma pytest pytest-cov
|
||||
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
||||
- name: Build Pillow
|
||||
run: CFLAGS="-coverage" python3 setup.py build_ext install
|
||||
|
||||
- name: Test Pillow
|
||||
run: |
|
||||
python3 selftest.py --installed
|
||||
python3 -c "from PIL import Image"
|
||||
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
|
||||
|
||||
- name: Upload coverage
|
||||
run: |
|
||||
python3 -m pip install codecov
|
||||
bash <(curl -s https://codecov.io/bash) -F GHA_Windows
|
||||
env:
|
||||
CODECOV_NAME: ${{ matrix.name }}
|
||||
|
||||
success:
|
||||
needs: [build, msys]
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Windows Test Successful
|
||||
steps:
|
||||
|
|
41
.github/workflows/test.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -9,54 +9,41 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
os: [
|
||||
"macos-latest",
|
||||
"ubuntu-latest",
|
||||
"macOS-latest",
|
||||
]
|
||||
python-version: [
|
||||
"pypy-3.8",
|
||||
"pypy-3.7",
|
||||
"pypy-3.6",
|
||||
"3.10-dev",
|
||||
"3.10",
|
||||
"3.9",
|
||||
"3.8",
|
||||
"3.7",
|
||||
"3.6",
|
||||
]
|
||||
include:
|
||||
- python-version: "3.6"
|
||||
- python-version: "3.7"
|
||||
PYTHONOPTIMIZE: 1
|
||||
REVERSE: "--reverse"
|
||||
- python-version: "3.7"
|
||||
- python-version: "3.8"
|
||||
PYTHONOPTIMIZE: 2
|
||||
# Include new variables for Codecov
|
||||
- os: ubuntu-latest
|
||||
codecov-flag: GHA_Ubuntu
|
||||
- os: macOS-latest
|
||||
- os: macos-latest
|
||||
codecov-flag: GHA_macOS
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Get pip cache dir
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(python3 -m pip cache dir)"
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key:
|
||||
${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.ci/*.sh') }}
|
||||
restore-keys: |
|
||||
${{ matrix.os }}-${{ matrix.python-version }}-
|
||||
cache: pip
|
||||
cache-dependency-path: ".ci/*.sh"
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
@ -97,16 +84,16 @@ jobs:
|
|||
mkdir -p Tests/errors
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
path: Tests/errors
|
||||
|
||||
- name: Docs
|
||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9
|
||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
|
||||
run: |
|
||||
python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
|
||||
python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
|
||||
make doccheck
|
||||
|
||||
- name: After success
|
||||
|
|
26
.github/workflows/tidelift.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
name: Tidelift Align
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 2 * * *" # daily at 02:30 UTC
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/tidelift.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/tidelift.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository_owner == 'python-pillow'
|
||||
name: Run Tidelift to ensure approved open source packages are in use
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Scan
|
||||
uses: tidelift/alignment-action@main
|
||||
env:
|
||||
TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }}
|
||||
TIDELIFT_ORGANIZATION: team/aclark4life
|
||||
TIDELIFT_PROJECT: pillow
|
1
.gitignore
vendored
|
@ -83,6 +83,7 @@ docs/_build/
|
|||
Tests/images/README.md
|
||||
Tests/images/crash_1.tif
|
||||
Tests/images/crash_2.tif
|
||||
Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif
|
||||
Tests/images/string_dimension.tiff
|
||||
Tests/images/jpeg2000
|
||||
Tests/images/msp
|
||||
|
|
|
@ -1,43 +1,43 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: e3000ace2fd1fcb1c181bb7a8285f1f976bcbdc7 # frozen: 21.7b0
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ["--target-version", "py36"]
|
||||
args: ["--target-version", "py37"]
|
||||
# Only .py files, until https://github.com/psf/black/issues/402 resolved
|
||||
files: \.py$
|
||||
types: []
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: fd5ba70665a37ec301a1f714ed09336048b3be63 # frozen: 5.9.3
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/asottile/yesqa
|
||||
rev: 644ede78511c02fc6f8e03e014cc1ddcfbf1e1f5 # frozen: v1.2.3
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
|
||||
rev: v1.1.13
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: dcd740bc0ebaf2b3d43e59a0060d157c97de13f3 # frozen: 3.9.2
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: 38b88246ccc552bffaaf54259d064beeee434539 # frozen: v4.0.1
|
||||
rev: v4.1.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
|
|
@ -1,2 +1,8 @@
|
|||
version: 2
|
||||
|
||||
python:
|
||||
pip_install: true
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
|
|
319
CHANGES.rst
|
@ -2,9 +2,300 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
8.4.0 (unreleased)
|
||||
9.2.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Round lut values where necessary #6188
|
||||
[radarhere]
|
||||
|
||||
- Load before getting size in resize() #6190
|
||||
[radarhere]
|
||||
|
||||
- Load image before performing size calculations in thumbnail() #6186
|
||||
[radarhere]
|
||||
|
||||
- Deprecated PhotoImage.paste() box parameter #6178
|
||||
[radarhere]
|
||||
|
||||
9.1.0 (2022-04-01)
|
||||
------------------
|
||||
|
||||
- Add support for multiple component transformation to JPEG2000 #5500
|
||||
[scaramallion, radarhere, hugovk]
|
||||
|
||||
- Fix loading FriBiDi on Alpine #6165
|
||||
[nulano]
|
||||
|
||||
- Added setting for converting GIF P frames to RGB #6150
|
||||
[radarhere]
|
||||
|
||||
- Allow 1 mode images to be inverted #6034
|
||||
[radarhere]
|
||||
|
||||
- Raise ValueError when trying to save empty JPEG #6159
|
||||
[radarhere]
|
||||
|
||||
- Always save TIFF with contiguous planar configuration #5973
|
||||
[radarhere]
|
||||
|
||||
- Connected discontiguous polygon corners #5980
|
||||
[radarhere]
|
||||
|
||||
- Ensure Tkinter hook is activated for getimage() #6032
|
||||
[radarhere]
|
||||
|
||||
- Use screencapture arguments to crop on macOS #6152
|
||||
[radarhere]
|
||||
|
||||
- Do not mark L mode JPEG as 1 bit in PDF #6151
|
||||
[radarhere]
|
||||
|
||||
- Added support for reading I;16R TIFF images #6132
|
||||
[radarhere]
|
||||
|
||||
- If an error occurs after creating a file, remove the file #6134
|
||||
[radarhere]
|
||||
|
||||
- Fixed calling DisplayViewer or XVViewer without a title #6136
|
||||
[radarhere]
|
||||
|
||||
- Retain RGBA transparency when saving multiple GIF frames #6128
|
||||
[radarhere]
|
||||
|
||||
- Save additional ICO frames with other bit depths if supplied #6122
|
||||
[radarhere]
|
||||
|
||||
- Handle EXIF data truncated to just the header #6124
|
||||
[radarhere]
|
||||
|
||||
- Added support for reading BMP images with RLE8 compression #6102
|
||||
[radarhere]
|
||||
|
||||
- Support Python distributions where _tkinter is compiled in #6006
|
||||
[lukegb]
|
||||
|
||||
- Added support for PPM arbitrary maxval #6119
|
||||
[radarhere]
|
||||
|
||||
- Added BigTIFF reading #6097
|
||||
[radarhere]
|
||||
|
||||
- When converting, clip I;16 to be unsigned, not signed #6112
|
||||
[radarhere]
|
||||
|
||||
- Fixed loading L mode GIF with transparency #6086
|
||||
[radarhere]
|
||||
|
||||
- Improved handling of PPM header #5121
|
||||
[Piolie, radarhere]
|
||||
|
||||
- Reset size when seeking away from "Large Thumbnail" MPO frame #6101
|
||||
[radarhere]
|
||||
|
||||
- Replace requirements.txt with extras #6072
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Added PyEncoder and support BLP saving #6069
|
||||
[radarhere]
|
||||
|
||||
- Handle TGA images with packets that cross scan lines #6087
|
||||
[radarhere]
|
||||
|
||||
- Added FITS reading #6056
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Added rawmode argument to Image.getpalette() #6061
|
||||
[radarhere]
|
||||
|
||||
- Fixed BUFR, GRIB and HDF5 stub saving #6071
|
||||
[radarhere]
|
||||
|
||||
- Do not automatically remove temporary ImageShow files on Unix #6045
|
||||
[radarhere]
|
||||
|
||||
- Correctly read JPEG compressed BLP images #4685
|
||||
[Meithal, radarhere]
|
||||
|
||||
- Merged _MODE_CONV typ into ImageMode as typestr #6057
|
||||
[radarhere]
|
||||
|
||||
- Consider palette size when converting and in getpalette() #6060
|
||||
[radarhere]
|
||||
|
||||
- Added enums #5954
|
||||
[radarhere]
|
||||
|
||||
- Ensure image is opaque after converting P to PA with RGB palette #6052
|
||||
[radarhere]
|
||||
|
||||
- Attach RGBA palettes from putpalette() when suitable #6054
|
||||
[radarhere]
|
||||
|
||||
- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030
|
||||
[radarhere]
|
||||
|
||||
- Drop excess values in BITSPERSAMPLE #6041
|
||||
[mikhail-iurkov]
|
||||
|
||||
- Added unpacker from RGBA;15 to RGB #6031
|
||||
[radarhere]
|
||||
|
||||
- Enable arm64 for MSVC on Windows #5811
|
||||
[gaborkertesz-linaro, gaborkertesz]
|
||||
|
||||
- Keep IPython/Jupyter text/plain output stable #5891
|
||||
[shamrin, radarhere]
|
||||
|
||||
- Raise an error when performing a negative crop #5972
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Deprecated show_file "file" argument in favour of "path" #5959
|
||||
[radarhere]
|
||||
|
||||
- Fixed SPIDER images for use with Bio-formats library #5956
|
||||
[radarhere]
|
||||
|
||||
- Ensure duplicated file pointer is closed #5946
|
||||
[radarhere]
|
||||
|
||||
- Added specific error if path coordinate type is incorrect #5942
|
||||
[radarhere]
|
||||
|
||||
- Return an empty bytestring from tobytes() for an empty image #5938
|
||||
[radarhere]
|
||||
|
||||
- Remove readonly from Image.__eq__ #5930
|
||||
[hugovk]
|
||||
|
||||
9.0.1 (2022-02-03)
|
||||
------------------
|
||||
|
||||
- In show_file, use os.remove to remove temporary images. CVE-2022-24303 #6010
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Restrict builtins within lambdas for ImageMath.eval. CVE-2022-22817 #6009
|
||||
[radarhere]
|
||||
|
||||
9.0.0 (2022-01-02)
|
||||
------------------
|
||||
|
||||
- Restrict builtins for ImageMath.eval(). CVE-2022-22817 #5923
|
||||
[radarhere]
|
||||
|
||||
- Ensure JpegImagePlugin stops at the end of a truncated file #5921
|
||||
[radarhere]
|
||||
|
||||
- Fixed ImagePath.Path array handling. CVE-2022-22815, CVE-2022-22816 #5920
|
||||
[radarhere]
|
||||
|
||||
- Remove consecutive duplicate tiles that only differ by their offset #5919
|
||||
[radarhere]
|
||||
|
||||
- Improved I;16 operations on big endian #5901
|
||||
[radarhere]
|
||||
|
||||
- Limit quantized palette to number of colors #5879
|
||||
[radarhere]
|
||||
|
||||
- Fixed palette index for zeroed color in FASTOCTREE quantize #5869
|
||||
[radarhere]
|
||||
|
||||
- When saving RGBA to GIF, make use of first transparent palette entry #5859
|
||||
[radarhere]
|
||||
|
||||
- Pass SAMPLEFORMAT to libtiff #5848
|
||||
[radarhere]
|
||||
|
||||
- Added rounding when converting P and PA #5824
|
||||
[radarhere]
|
||||
|
||||
- Improved putdata() documentation and data handling #5910
|
||||
[radarhere]
|
||||
|
||||
- Exclude carriage return in PDF regex to help prevent ReDoS #5912
|
||||
[hugovk]
|
||||
|
||||
- Fixed freeing pointer in ImageDraw.Outline.transform #5909
|
||||
[radarhere]
|
||||
|
||||
- Added ImageShow support for xdg-open #5897
|
||||
[m-shinder, radarhere]
|
||||
|
||||
- Support 16-bit grayscale ImageQt conversion #5856
|
||||
[cmbruns, radarhere]
|
||||
|
||||
- Convert subsequent GIF frames to RGB or RGBA #5857
|
||||
[radarhere]
|
||||
|
||||
- Do not prematurely return in ImageFile when saving to stdout #5665
|
||||
[infmagic2047, radarhere]
|
||||
|
||||
- Added support for top right and bottom right TGA orientations #5829
|
||||
[radarhere]
|
||||
|
||||
- Corrected ICNS file length in header #5845
|
||||
[radarhere]
|
||||
|
||||
- Block tile TIFF tags when saving #5839
|
||||
[radarhere]
|
||||
|
||||
- Added line width argument to polygon #5694
|
||||
[radarhere]
|
||||
|
||||
- Do not redeclare class each time when converting to NumPy #5844
|
||||
[radarhere]
|
||||
|
||||
- Only prevent repeated polygon pixels when drawing with transparency #5835
|
||||
[radarhere]
|
||||
|
||||
- Add support for pickling TrueType fonts #5826
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Only prefer command line tools SDK on macOS over default MacOSX SDK #5828
|
||||
[radarhere]
|
||||
|
||||
- Drop support for soon-EOL Python 3.6 #5768
|
||||
[hugovk, nulano, radarhere]
|
||||
|
||||
- Fix compilation on 64-bit Termux #5793
|
||||
[landfillbaby]
|
||||
|
||||
- Use title for display in ImageShow #5788
|
||||
[radarhere]
|
||||
|
||||
- Remove support for FreeType 2.7 and older #5777
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Fix for PyQt6 #5775
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Removed deprecated PILLOW_VERSION, Image.show command parameter, Image._showxv and ImageFile.raise_ioerror #5776
|
||||
[radarhere]
|
||||
|
||||
8.4.0 (2021-10-15)
|
||||
------------------
|
||||
|
||||
- Prefer global transparency in GIF when replacing with background color #5756
|
||||
[radarhere]
|
||||
|
||||
- Added "exif" keyword argument to TIFF saving #5575
|
||||
[radarhere]
|
||||
|
||||
- Copy Python palette to new image in quantize() #5696
|
||||
[radarhere]
|
||||
|
||||
- Read ICO AND mask from end #5667
|
||||
[radarhere]
|
||||
|
||||
- Actually check the framesize in FliDecode.c #5659
|
||||
[wiredfool]
|
||||
|
||||
- Determine JPEG2000 mode purely from ihdr header box #5654
|
||||
[radarhere]
|
||||
|
||||
- Fixed using info dictionary when writing multiple APNG frames #5611
|
||||
[radarhere]
|
||||
|
||||
- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655
|
||||
[radarhere]
|
||||
|
||||
|
@ -59,12 +350,30 @@ Changelog (Pillow)
|
|||
- Fixed ImageOps expand with tuple border on P image #5615
|
||||
[radarhere]
|
||||
|
||||
- Ensure TIFF RowsPerStrip is multiple of 8 for JPEG compression #5588
|
||||
[kmilos, radarhere]
|
||||
|
||||
- Fixed error saving APNG with duplicate frames and different duration times #5609
|
||||
[thak1411, radarhere]
|
||||
|
||||
8.3.2 (2021-09-02)
|
||||
------------------
|
||||
|
||||
- CVE-2021-23437 Raise ValueError if color specifier is too long
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Fix 6-byte OOB read in FliDecode
|
||||
[wiredfool]
|
||||
|
||||
- Add support for Python 3.10 #5569, #5570
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588
|
||||
[kmilos, radarhere]
|
||||
|
||||
- Updates for ``ImagePalette`` channel order #5599
|
||||
[radarhere]
|
||||
|
||||
- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651
|
||||
[nulano]
|
||||
|
||||
8.3.1 (2021-07-06)
|
||||
------------------
|
||||
|
||||
|
@ -338,7 +647,7 @@ Changelog (Pillow)
|
|||
- Changed Image.open formats parameter to be case-insensitive #5250
|
||||
[Piolie, radarhere]
|
||||
|
||||
- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-01-02) #5216
|
||||
- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-07-01) #5216
|
||||
[radarhere]
|
||||
|
||||
- Added tk version to pilinfo #5226
|
||||
|
|
2
LICENSE
|
@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
|
|||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2021 by Alex Clark and contributors
|
||||
Copyright © 2010-2022 by Alex Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source HPND License:
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
include *.c
|
||||
include *.h
|
||||
include *.in
|
||||
include *.lock
|
||||
include *.md
|
||||
include *.py
|
||||
include *.rst
|
||||
|
@ -9,6 +10,7 @@ include *.txt
|
|||
include *.yaml
|
||||
include LICENSE
|
||||
include Makefile
|
||||
include Pipfile
|
||||
include tox.ini
|
||||
graft Tests
|
||||
graft src
|
||||
|
|
66
Makefile
|
@ -9,9 +9,11 @@ clean:
|
|||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
pytest -qq
|
||||
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
|
||||
python3 -m pytest -qq
|
||||
rm -r htmlcov || true
|
||||
coverage report
|
||||
python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage
|
||||
python3 -m coverage report
|
||||
|
||||
.PHONY: doc
|
||||
doc:
|
||||
|
@ -33,33 +35,29 @@ help:
|
|||
@echo "Welcome to Pillow development. Please use \`make <target>\` where <target> is one of"
|
||||
@echo " clean remove build products"
|
||||
@echo " coverage run coverage test (in progress)"
|
||||
@echo " doc make html docs"
|
||||
@echo " docserve run an http server on the docs directory"
|
||||
@echo " doc make HTML docs"
|
||||
@echo " docserve run an HTTP server on the docs directory"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " inplace make inplace extension"
|
||||
@echo " install make and install"
|
||||
@echo " install-coverage make and install with C coverage"
|
||||
@echo " install-req install documentation and test dependencies"
|
||||
@echo " install-venv (deprecated) install in virtualenv"
|
||||
@echo " lint run the lint checks"
|
||||
@echo " lint-fix run black and isort to (mostly) fix lint issues."
|
||||
@echo " lint-fix run Black and isort to (mostly) fix lint issues"
|
||||
@echo " release-test run code and package tests before release"
|
||||
@echo " test run tests on installed pillow"
|
||||
@echo " upload build and upload sdists to PyPI"
|
||||
@echo " upload-test build and upload sdists to test.pythonpackages.com"
|
||||
@echo " test run tests on installed Pillow"
|
||||
|
||||
.PHONY: inplace
|
||||
inplace: clean
|
||||
python3 setup.py develop build_ext --inplace
|
||||
python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" .
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
python3 setup.py install
|
||||
python3 -m pip install .
|
||||
python3 selftest.py
|
||||
|
||||
.PHONY: install-coverage
|
||||
install-coverage:
|
||||
CFLAGS="-coverage -Werror=implicit-function-declaration" python3 setup.py build_ext install
|
||||
CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" .
|
||||
python3 selftest.py
|
||||
|
||||
.PHONY: debug
|
||||
|
@ -68,58 +66,52 @@ debug:
|
|||
# for our stuff, kills optimization, and redirects to dev null so we
|
||||
# see any build failures.
|
||||
make clean > /dev/null
|
||||
CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null
|
||||
|
||||
.PHONY: install-req
|
||||
install-req:
|
||||
python3 -m pip install -r requirements.txt
|
||||
|
||||
.PHONY: install-venv
|
||||
install-venv:
|
||||
echo "'install-venv' is deprecated and will be removed in a future Pillow release"
|
||||
virtualenv .
|
||||
bin/pip install -r requirements.txt
|
||||
CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null
|
||||
|
||||
.PHONY: release-test
|
||||
release-test:
|
||||
$(MAKE) install-req
|
||||
python3 setup.py develop
|
||||
python3 -m pip install -e .[tests]
|
||||
python3 selftest.py
|
||||
python3 -m pytest Tests
|
||||
python3 setup.py install
|
||||
python3 -m pip install .
|
||||
-rm dist/*.egg
|
||||
-rmdir dist
|
||||
python3 -m pytest -qq
|
||||
check-manifest
|
||||
pyroma .
|
||||
python3 -m check_manifest
|
||||
python3 -m pyroma .
|
||||
$(MAKE) readme
|
||||
|
||||
.PHONY: sdist
|
||||
sdist:
|
||||
python3 setup.py sdist --format=gztar
|
||||
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
|
||||
python3 -m build --sdist
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
pytest -qq
|
||||
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
|
||||
python3 -m pytest -qq
|
||||
|
||||
.PHONY: valgrind
|
||||
valgrind:
|
||||
python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind
|
||||
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
|
||||
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
|
||||
--log-file=/tmp/valgrind-output \
|
||||
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
|
||||
|
||||
.PHONY: readme
|
||||
readme:
|
||||
python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html
|
||||
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2
|
||||
python3 -m markdown2 README.md > .long-description.html && open .long-description.html
|
||||
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
tox --help > /dev/null || python3 -m pip install tox
|
||||
tox -e lint
|
||||
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
|
||||
python3 -m tox -e lint
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix:
|
||||
black --target-version py36 .
|
||||
isort .
|
||||
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
|
||||
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
|
||||
python3 -m black --target-version py37 .
|
||||
python3 -m isort .
|
||||
|
|
22
Pipfile
Normal file
|
@ -0,0 +1,22 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
black = "*"
|
||||
check-manifest = "*"
|
||||
coverage = "*"
|
||||
defusedxml = "*"
|
||||
packaging = "*"
|
||||
markdown2 = "*"
|
||||
olefile = "*"
|
||||
pyroma = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-timeout = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
324
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,324 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.9"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==21.2.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3",
|
||||
"sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==21.12b0"
|
||||
},
|
||||
"build": {
|
||||
"hashes": [
|
||||
"sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f",
|
||||
"sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||
],
|
||||
"version": "==2021.10.8"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
|
||||
"sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.9"
|
||||
},
|
||||
"check-manifest": {
|
||||
"hashes": [
|
||||
"sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95",
|
||||
"sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.47"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3",
|
||||
"sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==8.0.3"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0",
|
||||
"sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd",
|
||||
"sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884",
|
||||
"sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48",
|
||||
"sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76",
|
||||
"sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0",
|
||||
"sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64",
|
||||
"sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685",
|
||||
"sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47",
|
||||
"sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d",
|
||||
"sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840",
|
||||
"sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f",
|
||||
"sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971",
|
||||
"sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c",
|
||||
"sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a",
|
||||
"sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de",
|
||||
"sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17",
|
||||
"sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4",
|
||||
"sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521",
|
||||
"sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57",
|
||||
"sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b",
|
||||
"sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282",
|
||||
"sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644",
|
||||
"sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475",
|
||||
"sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d",
|
||||
"sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da",
|
||||
"sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953",
|
||||
"sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2",
|
||||
"sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e",
|
||||
"sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c",
|
||||
"sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc",
|
||||
"sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64",
|
||||
"sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74",
|
||||
"sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617",
|
||||
"sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3",
|
||||
"sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d",
|
||||
"sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa",
|
||||
"sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739",
|
||||
"sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8",
|
||||
"sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8",
|
||||
"sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781",
|
||||
"sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
|
||||
"sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
|
||||
"sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
|
||||
"sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
|
||||
"sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
|
||||
"sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.2"
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
||||
"sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.1"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
|
||||
"sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.18.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"markdown2": {
|
||||
"hashes": [
|
||||
"sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0",
|
||||
"sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.4.2"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"olefile": {
|
||||
"hashes": [
|
||||
"sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.46"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
|
||||
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
|
||||
],
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"pep517": {
|
||||
"hashes": [
|
||||
"sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0",
|
||||
"sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161"
|
||||
],
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2",
|
||||
"sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
|
||||
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
|
||||
"sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.10.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
|
||||
"sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.6"
|
||||
},
|
||||
"pyroma": {
|
||||
"hashes": [
|
||||
"sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65",
|
||||
"sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
|
||||
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.2.5"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
|
||||
"sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"pytest-timeout": {
|
||||
"hashes": [
|
||||
"sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112",
|
||||
"sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.26.0"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c",
|
||||
"sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==60.0.0"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f",
|
||||
"sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.2.3"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
|
||||
"sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.0.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
|
||||
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.26.7"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
32
README.md
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<img width="248" height="250" src="https://raw.githubusercontent.com/python-pillow/pillow-logo/master/pillow-logo-248x250.png" alt="Pillow logo">
|
||||
<img width="248" height="250" src="https://raw.githubusercontent.com/python-pillow/pillow-logo/main/pillow-logo-248x250.png" alt="Pillow logo">
|
||||
</p>
|
||||
|
||||
# Pillow
|
||||
|
@ -24,30 +24,36 @@ As of 2019, Pillow development is
|
|||
<tr>
|
||||
<th>tests</th>
|
||||
<td>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions?query=workflow%3ALint"><img
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/lint.yml"><img
|
||||
alt="GitHub Actions build status (Lint)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions?query=workflow%3ATest"><img
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test.yml"><img
|
||||
alt="GitHub Actions build status (Test Linux and macOS)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Windows%22"><img
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml"><img
|
||||
alt="GitHub Actions build status (Test Windows)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions?query=workflow%3A%22Test+Docker%22"><img
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
|
||||
alt="GitHub Actions build status (Test MinGW)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
|
||||
alt="GitHub Actions build status (Test Docker)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
|
||||
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
|
||||
alt="AppVeyor CI build status (Windows)"
|
||||
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/master.svg?label=Windows%20build"></a>
|
||||
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
|
||||
<a href="https://github.com/python-pillow/pillow-wheels/actions"><img
|
||||
alt="GitHub Actions wheels build status (Wheels)"
|
||||
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a>
|
||||
<a href="https://travis-ci.com/github/python-pillow/pillow-wheels"><img
|
||||
<a href="https://app.travis-ci.com/github/python-pillow/pillow-wheels"><img
|
||||
alt="Travis CI wheels build status (aarch64)"
|
||||
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/master.svg?label=aarch64%20wheels"></a>
|
||||
<a href="https://codecov.io/gh/python-pillow/Pillow"><img
|
||||
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels"></a>
|
||||
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
|
||||
alt="Code coverage"
|
||||
src="https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg"></a>
|
||||
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml"><img
|
||||
alt="Tidelift Align"
|
||||
src="https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml/badge.svg"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -93,12 +99,12 @@ The core image library is designed for fast access to data stored in a few basic
|
|||
- [Documentation](https://pillow.readthedocs.io/)
|
||||
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
|
||||
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
|
||||
- [Contribute](https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md)
|
||||
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
|
||||
- [Issues](https://github.com/python-pillow/Pillow/issues)
|
||||
- [Pull requests](https://github.com/python-pillow/Pillow/pulls)
|
||||
- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html)
|
||||
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
|
||||
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork)
|
||||
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
|
||||
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
|
||||
|
||||
## Report a Vulnerability
|
||||
|
||||
|
|
30
RELEASING.md
|
@ -8,8 +8,8 @@ information about how the version numbers line up with releases.
|
|||
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
|
||||
* [ ] Develop and prepare release in `master` 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 `master` 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 that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions.
|
||||
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Update `CHANGES.rst`.
|
||||
|
@ -24,13 +24,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions)
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.0*
|
||||
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)
|
||||
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
|
||||
|
@ -39,13 +39,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
|
||||
Released as needed for security, installation or critical bug fixes.
|
||||
|
||||
* [ ] Make necessary changes in `master` branch.
|
||||
* [ ] Make necessary changes in `main` branch.
|
||||
* [ ] Update `CHANGES.rst`.
|
||||
* [ ] Check out release branch e.g.:
|
||||
```bash
|
||||
git checkout -t remotes/origin/5.2.x
|
||||
```
|
||||
* [ ] Cherry pick individual commits from `master` branch to release branch e.g. `5.2.x`, then `git push`.
|
||||
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
|
||||
|
||||
|
||||
|
||||
|
@ -61,13 +61,13 @@ Released as needed for security, installation or critical bug fixes.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions)
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
```bash
|
||||
twine check dist/*
|
||||
twine upload dist/Pillow-5.2.1*
|
||||
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)
|
||||
|
||||
|
@ -76,7 +76,7 @@ Released as needed for security, installation or critical bug fixes.
|
|||
Released as needed privately to individual vendors for critical security-related bug fixes.
|
||||
|
||||
* [ ] Prepare patch for all versions that will get a fix. Test against local installations.
|
||||
* [ ] Commit against master, cherry pick to affected release branches.
|
||||
* [ ] Commit against `main`, cherry pick to affected release branches.
|
||||
* [ ] Run local test matrix on each release & Python version.
|
||||
* [ ] Privately send to distros.
|
||||
* [ ] Run pre-release check via `make release-test`
|
||||
|
@ -91,9 +91,9 @@ Released as needed privately to individual vendors for critical security-related
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
twine check dist/*
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions)
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
|
||||
## Binary Distributions
|
||||
|
|
|
@ -61,7 +61,7 @@ repro_copy = (
|
|||
|
||||
|
||||
for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy:
|
||||
im = Image.open(path)
|
||||
with Image.open(path) as im:
|
||||
try:
|
||||
im.load()
|
||||
except Exception as msg:
|
||||
|
|
|
@ -19,7 +19,7 @@ from PIL import Image
|
|||
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")
|
||||
|
||||
for path in repro:
|
||||
im = Image.open(path)
|
||||
with Image.open(path) as im:
|
||||
try:
|
||||
im.load()
|
||||
except Exception as msg:
|
||||
|
|
|
@ -30,7 +30,6 @@ if os.environ.get("SHOW_ERRORS", None):
|
|||
a.show()
|
||||
b.show()
|
||||
|
||||
|
||||
elif "GITHUB_ACTIONS" in os.environ:
|
||||
HAS_UPLOADER = True
|
||||
|
||||
|
@ -44,7 +43,6 @@ elif "GITHUB_ACTIONS" in os.environ:
|
|||
b.save(os.path.join(tmpdir, "b.png"))
|
||||
return tmpdir
|
||||
|
||||
|
||||
else:
|
||||
try:
|
||||
import test_image_results
|
||||
|
@ -326,7 +324,7 @@ def is_mingw():
|
|||
return sysconfig.get_platform() == "mingw"
|
||||
|
||||
|
||||
class cached_property:
|
||||
class CachedProperty:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
|
|
BIN
Tests/images/16bit.r.tif
Normal file
BIN
Tests/images/balloon_eciRGBv2_aware.jp2
Normal file
BIN
Tests/images/bitmap_font_stroke_basic.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
Tests/images/bitmap_font_stroke_raqm.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
Tests/images/blp/blp1_jpeg.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
Tests/images/crash-5762152299364352.fli
Normal file
BIN
Tests/images/cross_scan_line.png
Normal file
After Width: | Height: | Size: 71 B |
BIN
Tests/images/cross_scan_line.tga
Normal file
Before Width: | Height: | Size: 3.0 KiB |
BIN
Tests/images/different_transparency_merged.png
Normal file
After Width: | Height: | Size: 333 B |
BIN
Tests/images/dispose_bgnd_rgba.gif
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/dispose_bgnd_transparency.gif
Normal file
After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 17 KiB |
BIN
Tests/images/dispose_none_load_end_second.png
Normal file
After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 1.0 KiB |
BIN
Tests/images/dispose_prev_first_frame_seeked.png
Normal file
After Width: | Height: | Size: 208 B |
BIN
Tests/images/hopper_bigtiff.tif
Normal file
BIN
Tests/images/hopper_mask.ico
Normal file
After Width: | Height: | Size: 262 B |
BIN
Tests/images/hopper_mask.png
Normal file
After Width: | Height: | Size: 208 B |
BIN
Tests/images/hopper_rle8.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/hopper_rle8_row_overflow.bmp
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Tests/images/imagedraw/discontiguous_corners_polygon.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
Tests/images/imagedraw/triangle_right_width.png
Normal file
After Width: | Height: | Size: 472 B |
BIN
Tests/images/imagedraw/triangle_right_width_no_fill.png
Normal file
After Width: | Height: | Size: 479 B |
BIN
Tests/images/imagedraw_polygon_translucent.png
Normal file
After Width: | Height: | Size: 385 B |
Before Width: | Height: | Size: 950 B |
BIN
Tests/images/missing_background_first_frame.png
Normal file
After Width: | Height: | Size: 382 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
Tests/images/no_palette.gif
Normal file
After Width: | Height: | Size: 48 B |
BIN
Tests/images/no_palette_with_background.gif
Normal file
After Width: | Height: | Size: 54 B |
BIN
Tests/images/no_palette_with_transparency.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/pal8_offset.bmp
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
Tests/images/palette_negative.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/palette_sepia.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Tests/images/palette_wedge.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
Tests/images/rgb32rle_bottom_right.tga
Normal file
BIN
Tests/images/rgb32rle_top_right.tga
Normal file
BIN
Tests/images/tiff_wrong_bits_per_sample_2.tiff
Normal file
BIN
Tests/images/timeout-6646305047838720
Normal file
|
@ -22,7 +22,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
|
|||
fuzzer_basename=$(basename -s .py $fuzzer)
|
||||
fuzzer_package=${fuzzer_basename}.pkg
|
||||
pyinstaller \
|
||||
--add-binary /usr/local/lib/libjpeg.so.9:. \
|
||||
--add-binary /usr/local/lib/libjpeg.so.62.3.0:. \
|
||||
--add-binary /usr/local/lib/libfreetype.so.6:. \
|
||||
--add-binary /usr/local/lib/liblcms2.so.2:. \
|
||||
--add-binary /usr/local/lib/libopenjp2.so.7:. \
|
||||
|
|
|
@ -14,9 +14,12 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import atheris
|
||||
|
||||
with atheris.instrument_imports():
|
||||
import sys
|
||||
|
||||
import atheris_no_libfuzzer as atheris
|
||||
import fuzzers
|
||||
|
||||
|
||||
|
@ -26,13 +29,12 @@ def TestOneInput(data):
|
|||
except Exception:
|
||||
# We're catching all exceptions because Pillow's exceptions are
|
||||
# directly inheriting from Exception.
|
||||
return
|
||||
return
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
fuzzers.enable_decompressionbomb_error()
|
||||
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
|
||||
atheris.Setup(sys.argv, TestOneInput)
|
||||
atheris.Fuzz()
|
||||
fuzzers.disable_decompressionbomb_error()
|
||||
|
||||
|
|
|
@ -14,9 +14,12 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import atheris
|
||||
|
||||
with atheris.instrument_imports():
|
||||
import sys
|
||||
|
||||
import atheris_no_libfuzzer as atheris
|
||||
import fuzzers
|
||||
|
||||
|
||||
|
@ -26,13 +29,12 @@ def TestOneInput(data):
|
|||
except Exception:
|
||||
# We're catching all exceptions because Pillow's exceptions are
|
||||
# directly inheriting from Exception.
|
||||
return
|
||||
return
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
fuzzers.enable_decompressionbomb_error()
|
||||
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
|
||||
atheris.Setup(sys.argv, TestOneInput)
|
||||
atheris.Fuzz()
|
||||
fuzzers.disable_decompressionbomb_error()
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
import warnings
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
@ -20,16 +19,14 @@ def test_bad():
|
|||
either"""
|
||||
for f in get_files("b"):
|
||||
|
||||
with pytest.warns(None) as record:
|
||||
# Assert that there is no unclosed file warning
|
||||
with warnings.catch_warnings():
|
||||
try:
|
||||
with Image.open(f) as im:
|
||||
im.load()
|
||||
except Exception: # as msg:
|
||||
pass
|
||||
|
||||
# Assert that there is no unclosed file warning
|
||||
assert not record
|
||||
|
||||
|
||||
def test_questionable():
|
||||
"""These shouldn't crash/dos, but it's not well defined that these
|
||||
|
@ -43,6 +40,7 @@ def test_questionable():
|
|||
"rgb32fakealpha.bmp",
|
||||
"rgb24largepal.bmp",
|
||||
"pal8os2sp.bmp",
|
||||
"pal8rletrns.bmp",
|
||||
"rgb32bf-xbgr.bmp",
|
||||
]
|
||||
for f in get_files("q"):
|
||||
|
|
|
@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1):
|
|||
return image._new(image.im.box_blur(radius, n))
|
||||
|
||||
|
||||
def assertImage(im, data, delta=0):
|
||||
def assert_image(im, data, delta=0):
|
||||
it = iter(im.getdata())
|
||||
for data_row in data:
|
||||
im_row = [next(it) for _ in range(im.size[0])]
|
||||
|
@ -35,12 +35,12 @@ def assertImage(im, data, delta=0):
|
|||
next(it)
|
||||
|
||||
|
||||
def assertBlur(im, radius, data, passes=1, delta=0):
|
||||
def assert_blur(im, radius, data, passes=1, delta=0):
|
||||
# check grayscale image
|
||||
assertImage(box_blur(im, radius, passes), data, delta)
|
||||
assert_image(box_blur(im, radius, passes), data, delta)
|
||||
rgba = Image.merge("RGBA", (im, im, im, im))
|
||||
for band in box_blur(rgba, radius, passes).split():
|
||||
assertImage(band, data, delta)
|
||||
assert_image(band, data, delta)
|
||||
|
||||
|
||||
def test_color_modes():
|
||||
|
@ -64,7 +64,7 @@ def test_color_modes():
|
|||
|
||||
|
||||
def test_radius_0():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0,
|
||||
[
|
||||
|
@ -80,7 +80,7 @@ def test_radius_0():
|
|||
|
||||
|
||||
def test_radius_0_02():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.02,
|
||||
[
|
||||
|
@ -97,7 +97,7 @@ def test_radius_0_02():
|
|||
|
||||
|
||||
def test_radius_0_05():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.05,
|
||||
[
|
||||
|
@ -114,7 +114,7 @@ def test_radius_0_05():
|
|||
|
||||
|
||||
def test_radius_0_1():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.1,
|
||||
[
|
||||
|
@ -131,7 +131,7 @@ def test_radius_0_1():
|
|||
|
||||
|
||||
def test_radius_0_5():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.5,
|
||||
[
|
||||
|
@ -148,7 +148,7 @@ def test_radius_0_5():
|
|||
|
||||
|
||||
def test_radius_1():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
@ -165,7 +165,7 @@ def test_radius_1():
|
|||
|
||||
|
||||
def test_radius_1_5():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1.5,
|
||||
[
|
||||
|
@ -182,7 +182,7 @@ def test_radius_1_5():
|
|||
|
||||
|
||||
def test_radius_bigger_then_half():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
3,
|
||||
[
|
||||
|
@ -199,7 +199,7 @@ def test_radius_bigger_then_half():
|
|||
|
||||
|
||||
def test_radius_bigger_then_width():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
10,
|
||||
[
|
||||
|
@ -214,7 +214,7 @@ def test_radius_bigger_then_width():
|
|||
|
||||
|
||||
def test_extreme_large_radius():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
600,
|
||||
[
|
||||
|
@ -229,7 +229,7 @@ def test_extreme_large_radius():
|
|||
|
||||
|
||||
def test_two_passes():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
@ -247,7 +247,7 @@ def test_two_passes():
|
|||
|
||||
|
||||
def test_three_passes():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
|
|
@ -15,27 +15,27 @@ except ImportError:
|
|||
class TestColorLut3DCoreAPI:
|
||||
def generate_identity_table(self, channels, size):
|
||||
if isinstance(size, tuple):
|
||||
size1D, size2D, size3D = size
|
||||
size_1d, size_2d, size_3d = size
|
||||
else:
|
||||
size1D, size2D, size3D = (size, size, size)
|
||||
size_1d, size_2d, size_3d = (size, size, size)
|
||||
|
||||
table = [
|
||||
[
|
||||
r / (size1D - 1) if size1D != 1 else 0,
|
||||
g / (size2D - 1) if size2D != 1 else 0,
|
||||
b / (size3D - 1) if size3D != 1 else 0,
|
||||
r / (size1D - 1) if size1D != 1 else 0,
|
||||
g / (size2D - 1) if size2D != 1 else 0,
|
||||
r / (size_1d - 1) if size_1d != 1 else 0,
|
||||
g / (size_2d - 1) if size_2d != 1 else 0,
|
||||
b / (size_3d - 1) if size_3d != 1 else 0,
|
||||
r / (size_1d - 1) if size_1d != 1 else 0,
|
||||
g / (size_2d - 1) if size_2d != 1 else 0,
|
||||
][:channels]
|
||||
for b in range(size3D)
|
||||
for g in range(size2D)
|
||||
for r in range(size1D)
|
||||
for b in range(size_3d)
|
||||
for g in range(size_2d)
|
||||
for r in range(size_1d)
|
||||
]
|
||||
return (
|
||||
channels,
|
||||
size1D,
|
||||
size2D,
|
||||
size3D,
|
||||
size_1d,
|
||||
size_2d,
|
||||
size_3d,
|
||||
[item for sublist in table for item in sublist],
|
||||
)
|
||||
|
||||
|
@ -43,107 +43,158 @@ class TestColorLut3DCoreAPI:
|
|||
im = Image.new("RGB", (10, 10), 0)
|
||||
|
||||
with pytest.raises(ValueError, match="filter"):
|
||||
im.im.color_lut_3d("RGB", Image.CUBIC, *self.generate_identity_table(3, 3))
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="image mode"):
|
||||
im.im.color_lut_3d(
|
||||
"wrong", Image.LINEAR, *self.generate_identity_table(3, 3)
|
||||
"wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="table_channels"):
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(5, 3))
|
||||
|
||||
with pytest.raises(ValueError, match="table_channels"):
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(1, 3))
|
||||
|
||||
with pytest.raises(ValueError, match="table_channels"):
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(2, 3))
|
||||
|
||||
with pytest.raises(ValueError, match="Table size"):
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, (1, 3, 3))
|
||||
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="table_channels"):
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="table_channels"):
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Table size"):
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, (66, 3, 3))
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, (1, 3, 3)),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Table size"):
|
||||
im.im.color_lut_3d(
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, (66, 3, 3)),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 7)
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9)
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8)
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, 16)
|
||||
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
|
||||
|
||||
def test_correct_args(self):
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3))
|
||||
|
||||
im.im.color_lut_3d("CMYK", Image.LINEAR, *self.generate_identity_table(4, 3))
|
||||
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 3, 3))
|
||||
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, (65, 3, 3))
|
||||
"CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
||||
)
|
||||
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 65, 3))
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, (2, 3, 3)),
|
||||
)
|
||||
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 3, 65))
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, (65, 3, 3)),
|
||||
)
|
||||
|
||||
im.im.color_lut_3d(
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, (3, 65, 3)),
|
||||
)
|
||||
|
||||
im.im.color_lut_3d(
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, (3, 3, 65)),
|
||||
)
|
||||
|
||||
def test_wrong_mode(self):
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("L", (10, 10), 0)
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3))
|
||||
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3))
|
||||
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("L", (10, 10), 0)
|
||||
im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3))
|
||||
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
im.im.color_lut_3d(
|
||||
"RGBA", Image.LINEAR, *self.generate_identity_table(3, 3)
|
||||
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(4, 3))
|
||||
im.im.color_lut_3d(
|
||||
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("L", (10, 10), 0)
|
||||
im.im.color_lut_3d(
|
||||
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
im.im.color_lut_3d(
|
||||
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="wrong mode"):
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
||||
)
|
||||
|
||||
def test_correct_mode(self):
|
||||
im = Image.new("RGBA", (10, 10), 0)
|
||||
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(3, 3))
|
||||
im.im.color_lut_3d(
|
||||
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
im = Image.new("RGBA", (10, 10), 0)
|
||||
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3))
|
||||
im.im.color_lut_3d(
|
||||
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
||||
)
|
||||
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
im.im.color_lut_3d("HSV", Image.LINEAR, *self.generate_identity_table(3, 3))
|
||||
im.im.color_lut_3d(
|
||||
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
|
||||
)
|
||||
|
||||
im = Image.new("RGB", (10, 10), 0)
|
||||
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3))
|
||||
im.im.color_lut_3d(
|
||||
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
|
||||
)
|
||||
|
||||
def test_identities(self):
|
||||
g = Image.linear_gradient("L")
|
||||
im = Image.merge(
|
||||
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
|
||||
"RGB",
|
||||
[
|
||||
g,
|
||||
g.transpose(Image.Transpose.ROTATE_90),
|
||||
g.transpose(Image.Transpose.ROTATE_180),
|
||||
],
|
||||
)
|
||||
|
||||
# Fast test with small cubes
|
||||
|
@ -152,7 +203,9 @@ class TestColorLut3DCoreAPI:
|
|||
im,
|
||||
im._new(
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, size)
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, size),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
@ -162,7 +215,9 @@ class TestColorLut3DCoreAPI:
|
|||
im,
|
||||
im._new(
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 2, 65))
|
||||
"RGB",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, (2, 2, 65)),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
@ -170,7 +225,12 @@ class TestColorLut3DCoreAPI:
|
|||
def test_identities_4_channels(self):
|
||||
g = Image.linear_gradient("L")
|
||||
im = Image.merge(
|
||||
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
|
||||
"RGB",
|
||||
[
|
||||
g,
|
||||
g.transpose(Image.Transpose.ROTATE_90),
|
||||
g.transpose(Image.Transpose.ROTATE_180),
|
||||
],
|
||||
)
|
||||
|
||||
# Red channel copied to alpha
|
||||
|
@ -178,7 +238,9 @@ class TestColorLut3DCoreAPI:
|
|||
Image.merge("RGBA", (im.split() * 2)[:4]),
|
||||
im._new(
|
||||
im.im.color_lut_3d(
|
||||
"RGBA", Image.LINEAR, *self.generate_identity_table(4, 17)
|
||||
"RGBA",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(4, 17),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
@ -189,9 +251,9 @@ class TestColorLut3DCoreAPI:
|
|||
"RGBA",
|
||||
[
|
||||
g,
|
||||
g.transpose(Image.ROTATE_90),
|
||||
g.transpose(Image.ROTATE_180),
|
||||
g.transpose(Image.ROTATE_270),
|
||||
g.transpose(Image.Transpose.ROTATE_90),
|
||||
g.transpose(Image.Transpose.ROTATE_180),
|
||||
g.transpose(Image.Transpose.ROTATE_270),
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -199,7 +261,9 @@ class TestColorLut3DCoreAPI:
|
|||
im,
|
||||
im._new(
|
||||
im.im.color_lut_3d(
|
||||
"RGBA", Image.LINEAR, *self.generate_identity_table(3, 17)
|
||||
"RGBA",
|
||||
Image.Resampling.BILINEAR,
|
||||
*self.generate_identity_table(3, 17),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
@ -207,14 +271,19 @@ class TestColorLut3DCoreAPI:
|
|||
def test_channels_order(self):
|
||||
g = Image.linear_gradient("L")
|
||||
im = Image.merge(
|
||||
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
|
||||
"RGB",
|
||||
[
|
||||
g,
|
||||
g.transpose(Image.Transpose.ROTATE_90),
|
||||
g.transpose(Image.Transpose.ROTATE_180),
|
||||
],
|
||||
)
|
||||
|
||||
# Reverse channels by splitting and using table
|
||||
# fmt: off
|
||||
assert_image_equal(
|
||||
Image.merge('RGB', im.split()[::-1]),
|
||||
im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
|
||||
3, 2, 2, 2, [
|
||||
0, 0, 0, 0, 0, 1,
|
||||
0, 1, 0, 0, 1, 1,
|
||||
|
@ -227,11 +296,16 @@ class TestColorLut3DCoreAPI:
|
|||
def test_overflow(self):
|
||||
g = Image.linear_gradient("L")
|
||||
im = Image.merge(
|
||||
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
|
||||
"RGB",
|
||||
[
|
||||
g,
|
||||
g.transpose(Image.Transpose.ROTATE_90),
|
||||
g.transpose(Image.Transpose.ROTATE_180),
|
||||
],
|
||||
)
|
||||
|
||||
# fmt: off
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
|
||||
3, 2, 2, 2,
|
||||
[
|
||||
-1, -1, -1, 2, -1, -1,
|
||||
|
@ -251,7 +325,7 @@ class TestColorLut3DCoreAPI:
|
|||
assert transformed[205, 205] == (255, 255, 0)
|
||||
|
||||
# fmt: off
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
|
||||
3, 2, 2, 2,
|
||||
[
|
||||
-3, -3, -3, 5, -3, -3,
|
||||
|
@ -354,7 +428,12 @@ class TestColorLut3DFilter:
|
|||
def test_numpy_formats(self):
|
||||
g = Image.linear_gradient("L")
|
||||
im = Image.merge(
|
||||
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
|
||||
"RGB",
|
||||
[
|
||||
g,
|
||||
g.transpose(Image.Transpose.ROTATE_90),
|
||||
g.transpose(Image.Transpose.ROTATE_180),
|
||||
],
|
||||
)
|
||||
|
||||
lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b))
|
||||
|
@ -445,7 +524,12 @@ class TestGenerateColorLut3D:
|
|||
|
||||
g = Image.linear_gradient("L")
|
||||
im = Image.merge(
|
||||
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
|
||||
"RGB",
|
||||
[
|
||||
g,
|
||||
g.transpose(Image.Transpose.ROTATE_90),
|
||||
g.transpose(Image.Transpose.ROTATE_180),
|
||||
],
|
||||
)
|
||||
assert im == im.filter(lut)
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ class TestDecompressionCrop:
|
|||
def teardown_class(self):
|
||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||
|
||||
def testEnlargeCrop(self):
|
||||
def test_enlarge_crop(self):
|
||||
# Crops can extend the extents, therefore we should have the
|
||||
# same decompression bomb warnings on them.
|
||||
with hopper() as src:
|
||||
|
@ -86,21 +86,12 @@ class TestDecompressionCrop:
|
|||
pytest.warns(Image.DecompressionBombWarning, src.crop, box)
|
||||
|
||||
def test_crop_decompression_checks(self):
|
||||
|
||||
im = Image.new("RGB", (100, 100))
|
||||
|
||||
good_values = ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990))
|
||||
|
||||
warning_values = ((-160, -160, 99, 99), (160, 160, -99, -99))
|
||||
|
||||
error_values = ((-99909, -99990, 99999, 99999), (99909, 99990, -99999, -99999))
|
||||
|
||||
for value in good_values:
|
||||
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):
|
||||
assert im.crop(value).size == (9, 9)
|
||||
|
||||
for value in warning_values:
|
||||
pytest.warns(Image.DecompressionBombWarning, im.crop, value)
|
||||
pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99))
|
||||
|
||||
for value in error_values:
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
im.crop(value)
|
||||
im.crop((-99909, -99990, 99999, 99999))
|
||||
|
|
91
Tests/test_deprecate.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
import pytest
|
||||
|
||||
from PIL import _deprecate
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"version, expected",
|
||||
[
|
||||
(
|
||||
10,
|
||||
"Old thing is deprecated and will be removed in Pillow 10 "
|
||||
r"\(2023-07-01\)\. Use new thing instead\.",
|
||||
),
|
||||
(
|
||||
None,
|
||||
r"Old thing is deprecated and will be removed in a future version\. "
|
||||
r"Use new thing instead\.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_version(version, expected):
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", version, "new thing")
|
||||
|
||||
|
||||
def test_unknown_version():
|
||||
expected = r"Unknown removal version, update PIL\._deprecate\?"
|
||||
with pytest.raises(ValueError, match=expected):
|
||||
_deprecate.deprecate("Old thing", 12345, "new thing")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"deprecated, plural, expected",
|
||||
[
|
||||
(
|
||||
"Old thing",
|
||||
False,
|
||||
r"Old thing is deprecated and should be removed\.",
|
||||
),
|
||||
(
|
||||
"Old things",
|
||||
True,
|
||||
r"Old things are deprecated and should be removed\.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_old_version(deprecated, plural, expected):
|
||||
expected = r""
|
||||
with pytest.raises(RuntimeError, match=expected):
|
||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||
|
||||
|
||||
def test_plural():
|
||||
expected = (
|
||||
r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
|
||||
r"Use new thing instead\."
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old things", 10, "new thing", plural=True)
|
||||
|
||||
|
||||
def test_replacement_and_action():
|
||||
expected = "Use only one of 'replacement' and 'action'"
|
||||
with pytest.raises(ValueError, match=expected):
|
||||
_deprecate.deprecate(
|
||||
"Old thing", 10, replacement="new thing", action="Upgrade to new thing"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"action",
|
||||
[
|
||||
"Upgrade to new thing",
|
||||
"Upgrade to new thing.",
|
||||
],
|
||||
)
|
||||
def test_action(action):
|
||||
expected = (
|
||||
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
|
||||
r"Upgrade to new thing\."
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", 10, action=action)
|
||||
|
||||
|
||||
def test_no_replacement_or_action():
|
||||
expected = (
|
||||
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)"
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", 10)
|
|
@ -120,9 +120,9 @@ def test_apng_dispose_op_previous_frame():
|
|||
# save_all=True,
|
||||
# append_images=[green, blue],
|
||||
# disposal=[
|
||||
# PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
|
||||
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS
|
||||
# PngImagePlugin.Disposal.OP_NONE,
|
||||
# PngImagePlugin.Disposal.OP_PREVIOUS,
|
||||
# PngImagePlugin.Disposal.OP_PREVIOUS
|
||||
# ],
|
||||
# )
|
||||
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
|
||||
|
@ -441,6 +441,12 @@ def test_apng_save_duration_loop(tmp_path):
|
|||
assert im.n_frames == 1
|
||||
assert im.info.get("duration") == 750
|
||||
|
||||
# test info duration
|
||||
frame.info["duration"] = 750
|
||||
frame.save(test_file, save_all=True)
|
||||
with Image.open(test_file) as im:
|
||||
assert im.info.get("duration") == 750
|
||||
|
||||
|
||||
def test_apng_save_disposal(tmp_path):
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
@ -449,31 +455,31 @@ def test_apng_save_disposal(tmp_path):
|
|||
green = Image.new("RGBA", size, (0, 255, 0, 255))
|
||||
transparent = Image.new("RGBA", size, (0, 0, 0, 0))
|
||||
|
||||
# test APNG_DISPOSE_OP_NONE
|
||||
# test OP_NONE
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[green, transparent],
|
||||
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
disposal=PngImagePlugin.Disposal.OP_NONE,
|
||||
blend=PngImagePlugin.Blend.OP_OVER,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(2)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
# test APNG_DISPOSE_OP_BACKGROUND
|
||||
# test OP_BACKGROUND
|
||||
disposal = [
|
||||
PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND,
|
||||
PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
PngImagePlugin.Disposal.OP_NONE,
|
||||
PngImagePlugin.Disposal.OP_BACKGROUND,
|
||||
PngImagePlugin.Disposal.OP_NONE,
|
||||
]
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[red, transparent],
|
||||
disposal=disposal,
|
||||
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
blend=PngImagePlugin.Blend.OP_OVER,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(2)
|
||||
|
@ -481,26 +487,26 @@ def test_apng_save_disposal(tmp_path):
|
|||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
disposal = [
|
||||
PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND,
|
||||
PngImagePlugin.Disposal.OP_NONE,
|
||||
PngImagePlugin.Disposal.OP_BACKGROUND,
|
||||
]
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[green],
|
||||
disposal=disposal,
|
||||
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
blend=PngImagePlugin.Blend.OP_OVER,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
# test APNG_DISPOSE_OP_PREVIOUS
|
||||
# test OP_PREVIOUS
|
||||
disposal = [
|
||||
PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
|
||||
PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
PngImagePlugin.Disposal.OP_NONE,
|
||||
PngImagePlugin.Disposal.OP_PREVIOUS,
|
||||
PngImagePlugin.Disposal.OP_NONE,
|
||||
]
|
||||
red.save(
|
||||
test_file,
|
||||
|
@ -508,7 +514,7 @@ def test_apng_save_disposal(tmp_path):
|
|||
append_images=[green, red, transparent],
|
||||
default_image=True,
|
||||
disposal=disposal,
|
||||
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
blend=PngImagePlugin.Blend.OP_OVER,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(3)
|
||||
|
@ -516,21 +522,32 @@ def test_apng_save_disposal(tmp_path):
|
|||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
disposal = [
|
||||
PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
|
||||
PngImagePlugin.Disposal.OP_NONE,
|
||||
PngImagePlugin.Disposal.OP_PREVIOUS,
|
||||
]
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[green],
|
||||
disposal=disposal,
|
||||
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
blend=PngImagePlugin.Blend.OP_OVER,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
# test info disposal
|
||||
red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))],
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(1)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
|
||||
def test_apng_save_disposal_previous(tmp_path):
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
@ -539,12 +556,12 @@ def test_apng_save_disposal_previous(tmp_path):
|
|||
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
||||
green = Image.new("RGBA", size, (0, 255, 0, 255))
|
||||
|
||||
# test APNG_DISPOSE_OP_NONE
|
||||
# test OP_NONE
|
||||
transparent.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[red, green],
|
||||
disposal=PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
|
||||
disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(2)
|
||||
|
@ -559,17 +576,17 @@ def test_apng_save_blend(tmp_path):
|
|||
green = Image.new("RGBA", size, (0, 255, 0, 255))
|
||||
transparent = Image.new("RGBA", size, (0, 0, 0, 0))
|
||||
|
||||
# test APNG_BLEND_OP_SOURCE on solid color
|
||||
# test OP_SOURCE on solid color
|
||||
blend = [
|
||||
PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
PngImagePlugin.APNG_BLEND_OP_SOURCE,
|
||||
PngImagePlugin.Blend.OP_OVER,
|
||||
PngImagePlugin.Blend.OP_SOURCE,
|
||||
]
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[red, green],
|
||||
default_image=True,
|
||||
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
disposal=PngImagePlugin.Disposal.OP_NONE,
|
||||
blend=blend,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -577,17 +594,17 @@ def test_apng_save_blend(tmp_path):
|
|||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
# test APNG_BLEND_OP_SOURCE on transparent color
|
||||
# test OP_SOURCE on transparent color
|
||||
blend = [
|
||||
PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
PngImagePlugin.APNG_BLEND_OP_SOURCE,
|
||||
PngImagePlugin.Blend.OP_OVER,
|
||||
PngImagePlugin.Blend.OP_SOURCE,
|
||||
]
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[red, transparent],
|
||||
default_image=True,
|
||||
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
disposal=PngImagePlugin.Disposal.OP_NONE,
|
||||
blend=blend,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -595,14 +612,14 @@ def test_apng_save_blend(tmp_path):
|
|||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
# test APNG_BLEND_OP_OVER
|
||||
# test OP_OVER
|
||||
red.save(
|
||||
test_file,
|
||||
save_all=True,
|
||||
append_images=[green, transparent],
|
||||
default_image=True,
|
||||
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
|
||||
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
|
||||
disposal=PngImagePlugin.Disposal.OP_NONE,
|
||||
blend=PngImagePlugin.Blend.OP_OVER,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(1)
|
||||
|
@ -611,3 +628,20 @@ def test_apng_save_blend(tmp_path):
|
|||
im.seek(2)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
# test info blend
|
||||
red.info["blend"] = PngImagePlugin.Blend.OP_OVER
|
||||
red.save(test_file, save_all=True, append_images=[green, transparent])
|
||||
with Image.open(test_file) as im:
|
||||
im.seek(2)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
||||
|
||||
def test_constants_deprecation():
|
||||
for enum, prefix in {
|
||||
PngImagePlugin.Disposal: "APNG_DISPOSE_",
|
||||
PngImagePlugin.Blend: "APNG_BLEND_",
|
||||
}.items():
|
||||
for name in enum.__members__:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert getattr(PngImagePlugin, prefix + name) == enum[name]
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import BlpImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal_tofile
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_equal_tofile,
|
||||
assert_image_similar,
|
||||
hopper,
|
||||
)
|
||||
|
||||
|
||||
def test_load_blp1():
|
||||
with Image.open("Tests/images/blp/blp1_jpeg.blp") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
|
||||
|
||||
|
||||
def test_load_blp2_raw():
|
||||
|
@ -20,6 +30,28 @@ def test_load_blp2_dxt1a():
|
|||
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
|
||||
|
||||
|
||||
def test_save(tmp_path):
|
||||
f = str(tmp_path / "temp.blp")
|
||||
|
||||
for version in ("BLP1", "BLP2"):
|
||||
im = hopper("P")
|
||||
im.save(f, blp_version=version)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
assert_image_equal(im.convert("RGB"), reloaded)
|
||||
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
f = str(tmp_path / "temp.blp")
|
||||
im.convert("P").save(f, blp_version=version)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
assert_image_similar(im, reloaded, 8)
|
||||
|
||||
im = hopper()
|
||||
with pytest.raises(ValueError):
|
||||
im.save(f)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
[
|
||||
|
@ -37,3 +69,14 @@ def test_crashes(test_file):
|
|||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_constants_deprecation():
|
||||
for enum, prefix in {
|
||||
BlpImagePlugin.Format: "BLP_FORMAT_",
|
||||
BlpImagePlugin.Encoding: "BLP_ENCODING_",
|
||||
BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_",
|
||||
}.items():
|
||||
for name in enum.__members__:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert getattr(BlpImagePlugin, prefix + name) == enum[name]
|
||||
|
|
|
@ -4,7 +4,12 @@ import pytest
|
|||
|
||||
from PIL import BmpImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_equal_tofile,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
)
|
||||
|
||||
|
||||
def test_sanity(tmp_path):
|
||||
|
@ -123,3 +128,46 @@ def test_rgba_bitfields():
|
|||
im = Image.merge("RGB", (r, g, b))
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
||||
|
||||
|
||||
def test_rle8():
|
||||
with Image.open("Tests/images/hopper_rle8.bmp") as im:
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||
|
||||
# This test image has been manually hexedited
|
||||
# to have rows with too much data
|
||||
with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
|
||||
|
||||
# Signal end of bitmap before the image is finished
|
||||
with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
|
||||
data = fp.read(1063) + b"\x01"
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_name,length",
|
||||
(
|
||||
# EOF immediately after the header
|
||||
("Tests/images/hopper_rle8.bmp", 1078),
|
||||
# EOF during delta
|
||||
("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
|
||||
# EOF when reading data in absolute mode
|
||||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||
),
|
||||
)
|
||||
def test_rle8_eof(file_name, length):
|
||||
with open(file_name, "rb") as fp:
|
||||
data = fp.read(length)
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_offset():
|
||||
# This image has been hexedited
|
||||
# to exclude the palette size from the pixel data offset
|
||||
with Image.open("Tests/images/pal8_offset.bmp") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
|
||||
|
|
|
@ -45,3 +45,35 @@ def test_save(tmp_path):
|
|||
# Act / Assert: stub cannot save without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
im.save(tmpfile)
|
||||
|
||||
|
||||
def test_handler(tmp_path):
|
||||
class TestHandler:
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
||||
def open(self, im):
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
self.loaded = True
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im, fp, filename):
|
||||
self.saved = True
|
||||
|
||||
handler = TestHandler()
|
||||
BufrStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
|
||||
temp_file = str(tmp_path / "temp.bufr")
|
||||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
BufrStubImagePlugin._handler = None
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import DcxImagePlugin, Image
|
||||
|
@ -31,21 +33,17 @@ def test_unclosed_file():
|
|||
|
||||
|
||||
def test_closed_file():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
im = Image.open(TEST_FILE)
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.load()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||
|
|
|
@ -196,6 +196,13 @@ def test__accept_false():
|
|||
assert not output
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
DdsImagePlugin.DdsImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_short_header():
|
||||
"""Check a short header"""
|
||||
with open(TEST_FILE_DXT5, "rb") as f:
|
||||
|
|
|
@ -58,6 +58,15 @@ def test_sanity():
|
|||
assert image2_scale2.format == "EPS"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_load():
|
||||
with Image.open(FILE1) as im:
|
||||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
|
||||
# Test again now that it has already been loaded once
|
||||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
|
|
80
Tests/test_file_fits.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import FitsImagePlugin, FitsStubImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.fits"
|
||||
|
||||
|
||||
def test_open():
|
||||
# Act
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
||||
# Assert
|
||||
assert im.format == "FITS"
|
||||
assert im.size == (128, 128)
|
||||
assert im.mode == "L"
|
||||
|
||||
assert_image_equal(im, hopper("L"))
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(SyntaxError):
|
||||
FitsImagePlugin.FitsImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_truncated_fits():
|
||||
# No END to headers
|
||||
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
|
||||
with pytest.raises(OSError):
|
||||
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
|
||||
|
||||
|
||||
def test_naxis_zero():
|
||||
# This test image has been manually hexedited
|
||||
# to set the number of data axes to zero
|
||||
with pytest.raises(ValueError):
|
||||
with Image.open("Tests/images/hopper_naxis_zero.fits"):
|
||||
pass
|
||||
|
||||
|
||||
def test_stub_deprecated():
|
||||
class Handler:
|
||||
opened = False
|
||||
loaded = False
|
||||
|
||||
def open(self, im):
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
self.loaded = True
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
handler = Handler()
|
||||
with pytest.warns(DeprecationWarning):
|
||||
FitsStubImagePlugin.register_handler(handler)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert im.format == "FITS"
|
||||
assert im.size == (128, 128)
|
||||
assert im.mode == "L"
|
||||
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
|
||||
FitsStubImagePlugin._handler = None
|
||||
Image.register_open(
|
||||
FitsImagePlugin.FitsImageFile.format,
|
||||
FitsImagePlugin.FitsImageFile,
|
||||
FitsImagePlugin._accept,
|
||||
)
|
|
@ -1,63 +0,0 @@
|
|||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import FitsStubImagePlugin, Image
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.fits"
|
||||
|
||||
|
||||
def test_open():
|
||||
# Act
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
||||
# Assert
|
||||
assert im.format == "FITS"
|
||||
assert im.size == (128, 128)
|
||||
assert im.mode == "L"
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(SyntaxError):
|
||||
FitsStubImagePlugin.FITSStubImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_load():
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
||||
# Act / Assert: stub cannot load without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_truncated_fits():
|
||||
# No END to headers
|
||||
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
|
||||
with pytest.raises(OSError):
|
||||
FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data))
|
||||
|
||||
|
||||
def test_naxis_zero():
|
||||
# This test image has been manually hexedited
|
||||
# to set the number of data axes to zero
|
||||
with pytest.raises(ValueError):
|
||||
with Image.open("Tests/images/hopper_naxis_zero.fits"):
|
||||
pass
|
||||
|
||||
|
||||
def test_save():
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
dummy_fp = None
|
||||
dummy_filename = "dummy.filename"
|
||||
|
||||
# Act / Assert: stub cannot save without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
im.save(dummy_filename)
|
||||
with pytest.raises(OSError):
|
||||
FitsStubImagePlugin._save(im, dummy_fp, dummy_filename)
|
|
@ -1,3 +1,5 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import FliImagePlugin, Image
|
||||
|
@ -38,21 +40,17 @@ def test_unclosed_file():
|
|||
|
||||
|
||||
def test_closed_file():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
im = Image.open(static_test_file)
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(static_test_file) as im:
|
||||
im.load()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_tell():
|
||||
# Arrange
|
||||
|
@ -138,3 +136,16 @@ def test_timeouts(test_file):
|
|||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
[
|
||||
"Tests/images/crash-5762152299364352.fli",
|
||||
],
|
||||
)
|
||||
def test_crash(test_file):
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from PIL import Image
|
||||
import pytest
|
||||
|
||||
from PIL import FtexImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar
|
||||
|
||||
|
@ -12,3 +14,19 @@ def test_load_dxt1():
|
|||
with Image.open("Tests/images/ftex_dxt1.ftc") as im:
|
||||
with Image.open("Tests/images/ftex_dxt1.png") as target:
|
||||
assert_image_similar(im, target.convert("RGBA"), 15)
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
FtexImagePlugin.FtexImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_constants_deprecation():
|
||||
for enum, prefix in {
|
||||
FtexImagePlugin.Format: "FORMAT_",
|
||||
}.items():
|
||||
for name in enum.__members__:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert getattr(FtexImagePlugin, prefix + name) == enum[name]
|
||||
|
|
|
@ -5,20 +5,28 @@ from PIL import GbrImagePlugin, Image
|
|||
from .helper import assert_image_equal_tofile
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
GbrImagePlugin.GbrImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_gbr_file():
|
||||
with Image.open("Tests/images/gbr.gbr") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
||||
|
||||
|
||||
def test_load():
|
||||
with Image.open("Tests/images/gbr.gbr") as im:
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
# Test again now that it has already been loaded once
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
|
||||
def test_multiple_load_operations():
|
||||
with Image.open("Tests/images/gbr.gbr") as im:
|
||||
im.load()
|
||||
im.load()
|
||||
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
GbrImagePlugin.GbrImageFile(invalid_file)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import warnings
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
@ -39,21 +40,17 @@ def test_unclosed_file():
|
|||
|
||||
|
||||
def test_closed_file():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
im = Image.open(TEST_GIF)
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(TEST_GIF) as im:
|
||||
im.load()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
@ -62,6 +59,51 @@ def test_invalid_file():
|
|||
GifImagePlugin.GifImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_l_mode_transparency():
|
||||
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
assert im.info["transparency"] == 255
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
|
||||
|
||||
def test_strategy():
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
expected_zero = im.convert("RGB")
|
||||
|
||||
im.seek(1)
|
||||
expected_one = im.convert("RGB")
|
||||
|
||||
try:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "RGB"
|
||||
assert_image_equal(im, expected_zero)
|
||||
|
||||
GifImagePlugin.LOADING_STRATEGY = (
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
)
|
||||
# Stay in P mode with only a global palette
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "P"
|
||||
assert_image_equal(im.convert("RGB"), expected_one)
|
||||
|
||||
# Change to RGB mode when a frame has an individual palette
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
|
||||
def test_optimize():
|
||||
def test_grayscale(optimize):
|
||||
im = Image.new("L", (1, 1), 0)
|
||||
|
@ -163,6 +205,32 @@ def test_roundtrip_save_all(tmp_path):
|
|||
assert reread.n_frames == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path, mode",
|
||||
(
|
||||
("Tests/images/dispose_bgnd.gif", "RGB"),
|
||||
# Hexeditted copy of dispose_bgnd to add transparency
|
||||
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
|
||||
),
|
||||
)
|
||||
def test_loading_multiple_palettes(path, mode):
|
||||
with Image.open(path) as im:
|
||||
assert im.mode == "P"
|
||||
first_frame_colors = im.palette.colors.keys()
|
||||
original_color = im.convert("RGB").load()[0, 0]
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == mode
|
||||
if mode == "RGBA":
|
||||
im = im.convert("RGB")
|
||||
|
||||
# Check a color only from the old palette
|
||||
assert im.load()[0, 0] == original_color
|
||||
|
||||
# Check a color from the new palette
|
||||
assert im.load()[24, 24] not in first_frame_colors
|
||||
|
||||
|
||||
def test_headers_saving_for_animated_gifs(tmp_path):
|
||||
important_headers = ["background", "version", "duration", "loop"]
|
||||
# Multiframe image
|
||||
|
@ -184,8 +252,8 @@ def test_palette_handling(tmp_path):
|
|||
with Image.open(TEST_GIF) as im:
|
||||
im = im.convert("RGB")
|
||||
|
||||
im = im.resize((100, 100), Image.LANCZOS)
|
||||
im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256)
|
||||
im = im.resize((100, 100), Image.Resampling.LANCZOS)
|
||||
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
|
||||
|
||||
f = str(tmp_path / "temp.gif")
|
||||
im2.save(f, optimize=True)
|
||||
|
@ -285,6 +353,22 @@ def test_n_frames():
|
|||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
|
||||
def test_no_change():
|
||||
# Test n_frames does not change the image
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
im.seek(1)
|
||||
expected = im.copy()
|
||||
assert im.n_frames == 5
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
# Test is_animated does not change the image
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
im.seek(3)
|
||||
expected = im.copy()
|
||||
assert im.is_animated
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
|
||||
def test_eoferror():
|
||||
with Image.open(TEST_GIF) as im:
|
||||
n_frames = im.n_frames
|
||||
|
@ -324,7 +408,7 @@ def test_dispose_none_load_end():
|
|||
with Image.open("Tests/images/dispose_none_load_end.gif") as img:
|
||||
img.seek(1)
|
||||
|
||||
assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.gif")
|
||||
assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
|
||||
|
||||
|
||||
def test_dispose_background():
|
||||
|
@ -337,14 +421,45 @@ def test_dispose_background():
|
|||
pass
|
||||
|
||||
|
||||
def test_transparent_dispose():
|
||||
expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)]
|
||||
def test_dispose_background_transparency():
|
||||
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
|
||||
img.seek(2)
|
||||
px = img.load()
|
||||
assert px[35, 30][3] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"loading_strategy, expected_colors",
|
||||
(
|
||||
(
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
|
||||
(
|
||||
(2, 1, 2),
|
||||
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
|
||||
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
|
||||
),
|
||||
),
|
||||
(
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
|
||||
(
|
||||
(2, 1, 2),
|
||||
(0, 1, 0),
|
||||
(2, 1, 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_transparent_dispose(loading_strategy, expected_colors):
|
||||
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||
try:
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
for frame in range(3):
|
||||
img.seek(frame)
|
||||
for x in range(3):
|
||||
color = img.getpixel((x, 0))
|
||||
assert color == expected_colors[frame][x]
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
|
||||
|
||||
def test_dispose_previous():
|
||||
|
@ -361,7 +476,7 @@ def test_dispose_previous_first_frame():
|
|||
with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
|
||||
im.seek(1)
|
||||
assert_image_equal_tofile(
|
||||
im, "Tests/images/dispose_prev_first_frame_seeked.gif"
|
||||
im, "Tests/images/dispose_prev_first_frame_seeked.png"
|
||||
)
|
||||
|
||||
|
||||
|
@ -501,7 +616,7 @@ def test_dispose2_background(tmp_path):
|
|||
|
||||
with Image.open(out) as im:
|
||||
im.seek(1)
|
||||
assert im.getpixel((0, 0)) == 0
|
||||
assert im.getpixel((0, 0)) == (255, 0, 0)
|
||||
|
||||
|
||||
def test_transparency_in_second_frame():
|
||||
|
@ -510,9 +625,9 @@ def test_transparency_in_second_frame():
|
|||
|
||||
# Seek to the second frame
|
||||
im.seek(im.tell() + 1)
|
||||
assert im.info["transparency"] == 0
|
||||
assert "transparency" not in im.info
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif")
|
||||
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
|
||||
|
||||
|
||||
def test_no_transparency_in_second_frame():
|
||||
|
@ -684,31 +799,31 @@ def test_zero_comment_subblocks():
|
|||
def test_version(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
def assertVersionAfterSave(im, version):
|
||||
def assert_version_after_save(im, version):
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["version"] == version
|
||||
|
||||
# Test that GIF87a is used by default
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
# Test setting the version to 89a
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
im.info["version"] = b"89a"
|
||||
assertVersionAfterSave(im, b"GIF89a")
|
||||
assert_version_after_save(im, b"GIF89a")
|
||||
|
||||
# Test that adding a GIF89a feature changes the version
|
||||
im.info["transparency"] = 1
|
||||
assertVersionAfterSave(im, b"GIF89a")
|
||||
assert_version_after_save(im, b"GIF89a")
|
||||
|
||||
# Test that a GIF87a image is also saved in that format
|
||||
with Image.open("Tests/images/test.colors.gif") as im:
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
# Test that a GIF89a image is also saved in that format
|
||||
im.info["version"] = b"GIF89a"
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
|
||||
def test_append_images(tmp_path):
|
||||
|
@ -723,10 +838,10 @@ def test_append_images(tmp_path):
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
im.save(out, save_all=True, append_images=imGenerator(ims))
|
||||
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert reread.n_frames == 3
|
||||
|
@ -781,6 +896,17 @@ def test_rgb_transparency(tmp_path):
|
|||
assert "transparency" not in reloaded.info
|
||||
|
||||
|
||||
def test_rgba_transparency(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = hopper("P")
|
||||
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded.seek(1)
|
||||
assert_image_equal(hopper("P").convert("RGB"), reloaded)
|
||||
|
||||
|
||||
def test_bbox(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
|
@ -811,7 +937,7 @@ def test_palette_save_P(tmp_path):
|
|||
# Forcing a non-straight grayscale palette.
|
||||
|
||||
im = hopper("P")
|
||||
palette = bytes([255 - i // 3 for i in range(768)])
|
||||
palette = bytes(255 - i // 3 for i in range(768))
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im.save(out, palette=palette)
|
||||
|
@ -856,7 +982,7 @@ def test_palette_save_ImagePalette(tmp_path):
|
|||
|
||||
with Image.open(out) as reloaded:
|
||||
im.putpalette(palette)
|
||||
assert_image_equal(reloaded, im)
|
||||
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
||||
|
||||
|
||||
def test_save_I(tmp_path):
|
||||
|
@ -874,11 +1000,11 @@ def test_save_I(tmp_path):
|
|||
def test_getdata():
|
||||
# Test getheader/getdata against legacy values.
|
||||
# Create a 'P' image with holes in the palette.
|
||||
im = Image._wedge().resize((16, 16), Image.NEAREST)
|
||||
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
|
||||
im.putpalette(ImagePalette.ImagePalette("RGB"))
|
||||
im.info = {"background": 0}
|
||||
|
||||
passed_palette = bytes([255 - i // 3 for i in range(768)])
|
||||
passed_palette = bytes(255 - i // 3 for i in range(768))
|
||||
|
||||
GifImagePlugin._FORCE_OPTIMIZE = True
|
||||
try:
|
||||
|
@ -910,6 +1036,11 @@ def test_lzw_bits():
|
|||
def test_extents():
|
||||
with Image.open("Tests/images/test_extents.gif") as im:
|
||||
assert im.size == (100, 100)
|
||||
|
||||
# Check that n_frames does not change the size
|
||||
assert im.n_frames == 2
|
||||
assert im.size == (100, 100)
|
||||
|
||||
im.seek(1)
|
||||
assert im.size == (150, 150)
|
||||
|
||||
|
@ -919,4 +1050,14 @@ def test_missing_background():
|
|||
# but the disposal method is "Restore to background color"
|
||||
with Image.open("Tests/images/missing_background.gif") as im:
|
||||
im.seek(1)
|
||||
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.gif")
|
||||
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")
|
||||
|
||||
|
||||
def test_saving_rgba(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded_rgba = reloaded.convert("RGBA")
|
||||
assert reloaded_rgba.load()[0, 0][3] == 0
|
||||
|
|
|
@ -45,3 +45,35 @@ def test_save(tmp_path):
|
|||
# Act / Assert: stub cannot save without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
im.save(tmpfile)
|
||||
|
||||
|
||||
def test_handler(tmp_path):
|
||||
class TestHandler:
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
||||
def open(self, im):
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
self.loaded = True
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im, fp, filename):
|
||||
self.saved = True
|
||||
|
||||
handler = TestHandler()
|
||||
GribStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
|
||||
temp_file = str(tmp_path / "temp.grib")
|
||||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
GribStubImagePlugin._handler = None
|
||||
|
|
|
@ -46,3 +46,35 @@ def test_save():
|
|||
im.save(dummy_filename)
|
||||
with pytest.raises(OSError):
|
||||
Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename)
|
||||
|
||||
|
||||
def test_handler(tmp_path):
|
||||
class TestHandler:
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
||||
def open(self, im):
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
self.loaded = True
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im, fp, filename):
|
||||
self.saved = True
|
||||
|
||||
handler = TestHandler()
|
||||
Hdf5StubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
|
||||
temp_file = str(tmp_path / "temp.h5")
|
||||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
Hdf5StubImagePlugin._handler = None
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import io
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import IcnsImagePlugin, Image, features
|
||||
from PIL import IcnsImagePlugin, Image, _binary, features
|
||||
|
||||
from .helper import assert_image_equal, assert_image_similar_tofile
|
||||
|
||||
|
@ -18,15 +20,22 @@ def test_sanity():
|
|||
with Image.open(TEST_FILE) as im:
|
||||
|
||||
# Assert that there is no unclosed file warning
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
im.load()
|
||||
assert not record
|
||||
|
||||
assert im.mode == "RGBA"
|
||||
assert im.size == (1024, 1024)
|
||||
assert im.format == "ICNS"
|
||||
|
||||
|
||||
def test_load():
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
# Test again now that it has already been loaded once
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
|
||||
def test_save(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.icns")
|
||||
|
||||
|
@ -38,6 +47,11 @@ def test_save(tmp_path):
|
|||
assert reread.size == (1024, 1024)
|
||||
assert reread.format == "ICNS"
|
||||
|
||||
file_length = os.path.getsize(temp_file)
|
||||
with open(temp_file, "rb") as fp:
|
||||
fp.seek(4)
|
||||
assert _binary.i32be(fp.read(4)) == file_length
|
||||
|
||||
|
||||
def test_save_append_images(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.icns")
|
||||
|
@ -98,12 +112,9 @@ def test_older_icon():
|
|||
|
||||
|
||||
def test_jp2_icon():
|
||||
# This icon was made by using Uli Kusterer's oldiconutil to replace
|
||||
# the PNG images with JPEG 2000 ones. The advantage of doing this is
|
||||
# that OS X 10.5 supports JPEG 2000 but not PNG; some commercial
|
||||
# software therefore does just this.
|
||||
|
||||
# (oldiconutil is here: https://github.com/uliwitness/oldiconutil)
|
||||
# This icon uses JPEG 2000 images instead of the PNG images.
|
||||
# The advantage of doing this is that OS X 10.5 supports JPEG 2000
|
||||
# but not PNG; some commercial software therefore does just this.
|
||||
|
||||
if not ENABLE_JPEG2K:
|
||||
return
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import io
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -18,6 +19,16 @@ def test_sanity():
|
|||
assert im.get_format_mimetype() == "image/x-icon"
|
||||
|
||||
|
||||
def test_load():
|
||||
with Image.open(TEST_ICO_FILE) as im:
|
||||
assert im.load()[0, 0] == (1, 1, 9, 255)
|
||||
|
||||
|
||||
def test_mask():
|
||||
with Image.open("Tests/images/hopper_mask.ico") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper_mask.png")
|
||||
|
||||
|
||||
def test_black_and_white():
|
||||
with Image.open("Tests/images/black_and_white.ico") as im:
|
||||
assert im.mode == "RGBA"
|
||||
|
@ -43,7 +54,9 @@ def test_save_to_bytes():
|
|||
assert im.mode == reloaded.mode
|
||||
assert (64, 64) == reloaded.size
|
||||
assert reloaded.format == "ICO"
|
||||
assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS))
|
||||
assert_image_equal(
|
||||
reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS)
|
||||
)
|
||||
|
||||
# The other one
|
||||
output.seek(0)
|
||||
|
@ -53,7 +66,56 @@ def test_save_to_bytes():
|
|||
assert im.mode == reloaded.mode
|
||||
assert (32, 32) == reloaded.size
|
||||
assert reloaded.format == "ICO"
|
||||
assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS))
|
||||
assert_image_equal(
|
||||
reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS)
|
||||
)
|
||||
|
||||
|
||||
def test_no_duplicates(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
|
||||
im = hopper()
|
||||
sizes = [(32, 32), (64, 64)]
|
||||
im.save(temp_file, "ico", sizes=sizes)
|
||||
|
||||
sizes.append(sizes[-1])
|
||||
im.save(temp_file2, "ico", sizes=sizes)
|
||||
|
||||
assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
|
||||
|
||||
|
||||
def test_different_bit_depths(tmp_path):
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
|
||||
im = hopper()
|
||||
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
|
||||
hopper("1").save(
|
||||
temp_file2,
|
||||
"ico",
|
||||
bitmap_format="bmp",
|
||||
sizes=[(128, 128)],
|
||||
append_images=[im],
|
||||
)
|
||||
|
||||
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
|
||||
|
||||
# Test that only matching sizes of different bit depths are saved
|
||||
temp_file3 = str(tmp_path / "temp3.ico")
|
||||
temp_file4 = str(tmp_path / "temp4.ico")
|
||||
|
||||
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
im.save(
|
||||
temp_file4,
|
||||
"ico",
|
||||
bitmap_format="bmp",
|
||||
sizes=[(128, 128)],
|
||||
append_images=[Image.new("P", (64, 64))],
|
||||
)
|
||||
|
||||
assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
||||
|
@ -70,7 +132,7 @@ def test_save_to_bytes_bmp(mode):
|
|||
assert "RGBA" == reloaded.mode
|
||||
assert (64, 64) == reloaded.size
|
||||
assert reloaded.format == "ICO"
|
||||
im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA")
|
||||
im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA")
|
||||
assert_image_equal(reloaded, im)
|
||||
|
||||
# The other one
|
||||
|
@ -81,7 +143,7 @@ def test_save_to_bytes_bmp(mode):
|
|||
assert "RGBA" == reloaded.mode
|
||||
assert (32, 32) == reloaded.size
|
||||
assert reloaded.format == "ICO"
|
||||
im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA")
|
||||
im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA")
|
||||
assert_image_equal(reloaded, im)
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import filecmp
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -35,21 +36,17 @@ def test_unclosed_file():
|
|||
|
||||
|
||||
def test_closed_file():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
im = Image.open(TEST_IM)
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(TEST_IM) as im:
|
||||
im.load()
|
||||
|
||||
assert not record
|
||||
|
||||
|
||||
def test_tell():
|
||||
# Arrange
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import re
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
@ -67,6 +68,13 @@ class TestFileJpeg:
|
|||
assert im.format == "JPEG"
|
||||
assert im.get_format_mimetype() == "image/jpeg"
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero(self, size, tmp_path):
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
im = Image.new("RGB", size)
|
||||
with pytest.raises(ValueError):
|
||||
im.save(f)
|
||||
|
||||
def test_app(self):
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
@ -85,26 +93,26 @@ class TestFileJpeg:
|
|||
f = "Tests/images/pil_sample_cmyk.jpg"
|
||||
with Image.open(f) as im:
|
||||
# the source image has red pixels in the upper left corner.
|
||||
c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))]
|
||||
c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
|
||||
assert c == 0.0
|
||||
assert m > 0.8
|
||||
assert y > 0.8
|
||||
assert k == 0.0
|
||||
# the opposite corner is black
|
||||
c, m, y, k = [
|
||||
c, m, y, k = (
|
||||
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
|
||||
]
|
||||
)
|
||||
assert k > 0.9
|
||||
# roundtrip, and check again
|
||||
im = self.roundtrip(im)
|
||||
c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))]
|
||||
c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
|
||||
assert c == 0.0
|
||||
assert m > 0.8
|
||||
assert y > 0.8
|
||||
assert k == 0.0
|
||||
c, m, y, k = [
|
||||
c, m, y, k = (
|
||||
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
|
||||
]
|
||||
)
|
||||
assert k > 0.9
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -271,7 +279,7 @@ class TestFileJpeg:
|
|||
del exif[0x8769]
|
||||
|
||||
# Assert that it needs to be transposed
|
||||
assert exif[0x0112] == Image.TRANSVERSE
|
||||
assert exif[0x0112] == Image.Transpose.TRANSVERSE
|
||||
|
||||
# Assert that the GPS IFD is present and empty
|
||||
assert exif.get_ifd(0x8825) == {}
|
||||
|
@ -756,9 +764,8 @@ class TestFileJpeg:
|
|||
assert exif[282] == 180
|
||||
|
||||
out = str(tmp_path / "out.jpg")
|
||||
with pytest.warns(None) as record:
|
||||
with warnings.catch_warnings():
|
||||
im.save(out, exif=exif)
|
||||
assert not record
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.getexif()[282] == 180
|
||||
|
@ -870,6 +877,30 @@ class TestFileJpeg:
|
|||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
assert im.getxmp() == {}
|
||||
|
||||
@pytest.mark.timeout(timeout=1)
|
||||
def test_eof(self):
|
||||
# Even though this decoder never says that it is finished
|
||||
# the image should still end when there is no new data
|
||||
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
|
||||
def decode(self, buffer):
|
||||
return 0, 0
|
||||
|
||||
decoder = InfiniteMockPyDecoder(None)
|
||||
|
||||
def closure(mode, *args):
|
||||
decoder.__init__(mode, *args)
|
||||
return decoder
|
||||
|
||||
Image.register_decoder("INFINITE", closure)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.tile = [
|
||||
("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
|
||||
]
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
im.load()
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_win32(), reason="Windows only")
|
||||
@skip_unless_feature("jpg")
|
||||
|
|