Merge branch 'main' into master

This commit is contained in:
Andrew Murray 2022-04-17 12:23:19 +10:00 committed by GitHub
commit 0bfbea0ab9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
322 changed files with 8177 additions and 5928 deletions

View File

@ -10,29 +10,29 @@ environment:
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES
matrix: matrix:
- PYTHON: C:/Python39 - PYTHON: C:/Python310
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python36-x64 - PYTHON: C:/Python37-x64
ARCHITECTURE: x64 ARCHITECTURE: x64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
install: 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:\ - 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 - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs9540w32.exe /S - ..\pillow-depends\gs9561w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.54.0\bin;%PATH% - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0) $host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH% - path C:\pillow\winbuild\build\bin;%PATH%
- '%PYTHON%\%EXECUTABLE% -m pip install -U setuptools'
build_script: build_script:
- ps: | - ps: |
@ -43,7 +43,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - 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% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
@ -84,7 +84,7 @@ deploy:
artifact: /.*egg|wheel/ artifact: /.*egg|wheel/
on: on:
APPVEYOR_REPO_NAME: python-pillow/Pillow APPVEYOR_REPO_NAME: python-pillow/Pillow
branch: master branch: main
deploy: YES deploy: YES

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# gather the coverage data # gather the coverage data
pip3 install codecov python3 -m pip install codecov
if [[ $MATRIX_DOCKER ]]; then if [[ $MATRIX_DOCKER ]]; then
coverage xml --ignore-errors coverage xml --ignore-errors
else else

View File

@ -19,7 +19,7 @@ set -e
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ 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 pip
python3 -m pip install --upgrade wheel 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 -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install test-image-results python3 -m pip install test-image-results
# TODO Remove condition when numpy supports 3.10 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
# PyQt5 doesn't support PyPy3 # PyQt5 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

View File

@ -4,13 +4,13 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
## Bug fixes, feature additions, etc. ## 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. - Fork the Pillow repository.
- Create a branch from master. - Create a branch from `main`.
- Develop bug fixes, features, tests, etc. - 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. - 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 ### Guidelines
@ -18,7 +18,7 @@ Please send a pull request to the master branch. Please include [documentation](
- Provide tests for any newly added code. - Provide tests for any newly added code.
- Follow PEP 8. - Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - 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 ## Reporting Issues
@ -35,4 +35,4 @@ The best reproductions are self-contained scripts with minimal dependencies. If
## Security vulnerabilities ## 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
View File

@ -7,6 +7,7 @@ pull_request_rules:
- status-success=Test Successful - status-success=Test Successful
- status-success=Docker Test Successful - status-success=Docker Test Successful
- status-success=Windows Test Successful - status-success=Windows Test Successful
- status-success=MinGW Test Successful
- status-success=continuous-integration/appveyor/pr - status-success=continuous-integration/appveyor/pr
actions: actions:
merge: merge:

View File

@ -1,4 +1,5 @@
name: CIFuzz name: CIFuzz
on: on:
push: push:
paths: paths:
@ -8,6 +9,7 @@ on:
paths: paths:
- "**.c" - "**.c"
- "**.h" - "**.h"
workflow_dispatch:
jobs: jobs:
Fuzzing: Fuzzing:
@ -29,13 +31,13 @@ jobs:
language: python language: python
dry-run: false dry-run: false
- name: Upload New Crash - name: Upload New Crash
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() && steps.build.outcome == 'success' if: failure() && steps.build.outcome == 'success'
with: with:
name: artifacts name: artifacts
path: ./out/artifacts path: ./out/artifacts
- name: Upload Legacy Crash - name: Upload Legacy Crash
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: steps.run.outcome == 'success' if: steps.run.outcome == 'success'
with: with:
name: crash name: crash

View File

@ -1,6 +1,6 @@
name: Lint name: Lint
on: [push, pull_request] on: [push, pull_request, workflow_dispatch]
jobs: jobs:
build: build:
@ -10,15 +10,7 @@ jobs:
name: Lint name: Lint
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: pip cache
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: lint-pip-${{ hashFiles('**/setup.py') }}
restore-keys: |
lint-pip-
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v2 uses: actions/cache@v2
@ -29,9 +21,11 @@ jobs:
lint-pre-commit- lint-pre-commit-
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: 3.8 python-version: "3.10"
cache: pip
cache-dependency-path: "setup.py"
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
@ -45,4 +39,3 @@ jobs:
run: tox -e lint run: tox -e lint
env: env:
PRE_COMMIT_COLOR: always PRE_COMMIT_COLOR: always

View File

@ -4,14 +4,15 @@ on:
push: push:
# branches to consider in the event; optional, defaults to all # branches to consider in the event; optional, defaults to all
branches: branches:
- master - main
workflow_dispatch:
jobs: jobs:
update_release_draft: update_release_draft:
if: github.repository == 'python-pillow/Pillow' if: github.repository == 'python-pillow/Pillow'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - uses: release-drafter/release-drafter@v5
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

27
.github/workflows/stale.yml vendored Normal file
View 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"

View File

@ -1,6 +1,6 @@
name: Test Docker name: Test Docker
on: [push, pull_request] on: [push, pull_request, workflow_dispatch]
jobs: jobs:
build: build:
@ -19,14 +19,16 @@ jobs:
amazon-2-amd64, amazon-2-amd64,
arch, arch,
centos-7-amd64, centos-7-amd64,
centos-8-amd64, centos-stream-8-amd64,
centos-stream-9-amd64,
debian-10-buster-x86, debian-10-buster-x86,
fedora-33-amd64, debian-11-bullseye-x86,
fedora-34-amd64, fedora-35-amd64,
gentoo,
ubuntu-18.04-bionic-amd64, ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
] ]
dockerTag: [master] dockerTag: [main]
include: include:
- docker: "ubuntu-20.04-focal-arm64v8" - docker: "ubuntu-20.04-focal-arm64v8"
qemu-arch: "aarch64" qemu-arch: "aarch64"
@ -38,7 +40,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

85
.github/workflows/test-mingw.yml vendored Normal file
View 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

View File

@ -11,6 +11,7 @@ on:
paths: paths:
- "**.c" - "**.c"
- "**.h" - "**.h"
workflow_dispatch:
jobs: jobs:
build: build:
@ -22,12 +23,12 @@ jobs:
docker: [ docker: [
ubuntu-20.04-focal-amd64-valgrind, ubuntu-20.04-focal-amd64-valgrind,
] ]
dockerTag: [master] dockerTag: [main]
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
@ -42,11 +43,3 @@ jobs:
sudo chown -R 1000 $GITHUB_WORKSPACE sudo chown -R 1000 $GITHUB_WORKSPACE
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE 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

View File

@ -1,52 +1,44 @@
name: Test Windows name: Test Windows
on: [push, pull_request] on: [push, pull_request, workflow_dispatch]
jobs: jobs:
build: build:
runs-on: windows-2019 runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: 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"] architecture: ["x86", "x64"]
include: 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 # PyPy 7.3.4+ only ships 64-bit binaries for Windows
- python-version: "pypy-3.7" - python-version: "pypy-3.7"
architecture: "x64" architecture: "x64"
- python-version: "pypy-3.8"
architecture: "x64"
timeout-minutes: 30 timeout-minutes: 30
name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} name: Python ${{ matrix.python-version }} ${{ matrix.architecture }}
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\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 # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }} architecture: ${{ matrix.architecture }}
cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information - name: Print build system information
run: python .github/workflows/system-info.py 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\" 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
winbuild\depends\gs9540w32.exe /S winbuild\depends\gs9561w32.exe /S
echo "C:\Program Files (x86)\gs\gs9.54.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\ xcopy /S /Y winbuild\depends\test_images\* Tests\images\
@ -140,15 +132,16 @@ jobs:
- name: Build Pillow - name: Build Pillow
run: | run: |
$FLAGS="" $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 & winbuild\build\build_pillow.cmd $FLAGS install
& $env:pythonLocation\python.exe selftest.py --installed & $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh shell: pwsh
# failing with PyPy3 # skip PyPy for speed
- name: Enable heap verification - name: Enable heap verification
if: "!contains(matrix.python-version, 'pypy')" 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 - name: Test Pillow
run: | run: |
@ -163,7 +156,7 @@ jobs:
shell: bash shell: bash
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: errors name: errors
@ -183,92 +176,20 @@ jobs:
- name: Build wheel - name: Build wheel
id: wheel id: wheel
if: "github.event_name == 'push'" if: "github.event_name != 'pull_request'"
run: | run: |
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd shell: cmd
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v3
if: "github.event_name == 'push'" if: "github.event_name != 'pull_request'"
with: with:
name: ${{ steps.wheel.outputs.dist }} name: ${{ steps.wheel.outputs.dist }}
path: dist\*.whl 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: success:
needs: [build, msys] needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Windows Test Successful name: Windows Test Successful
steps: steps:

View File

@ -1,6 +1,6 @@
name: Test name: Test
on: [push, pull_request] on: [push, pull_request, workflow_dispatch]
jobs: jobs:
build: build:
@ -9,54 +9,41 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ os: [
"macos-latest",
"ubuntu-latest", "ubuntu-latest",
"macOS-latest",
] ]
python-version: [ python-version: [
"pypy-3.8",
"pypy-3.7", "pypy-3.7",
"pypy-3.6", "3.10",
"3.10-dev",
"3.9", "3.9",
"3.8", "3.8",
"3.7", "3.7",
"3.6",
] ]
include: include:
- python-version: "3.6" - python-version: "3.7"
PYTHONOPTIMIZE: 1 PYTHONOPTIMIZE: 1
REVERSE: "--reverse" REVERSE: "--reverse"
- python-version: "3.7" - python-version: "3.8"
PYTHONOPTIMIZE: 2 PYTHONOPTIMIZE: 2
# Include new variables for Codecov # Include new variables for Codecov
- os: ubuntu-latest - os: ubuntu-latest
codecov-flag: GHA_Ubuntu codecov-flag: GHA_Ubuntu
- os: macOS-latest - os: macos-latest
codecov-flag: GHA_macOS codecov-flag: GHA_macOS
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
cache: pip
- name: Get pip cache dir cache-dependency-path: ".ci/*.sh"
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 }}-
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
@ -97,16 +84,16 @@ jobs:
mkdir -p Tests/errors mkdir -p Tests/errors
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: errors name: errors
path: Tests/errors path: Tests/errors
- name: Docs - name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: | 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 make doccheck
- name: After success - name: After success

26
.github/workflows/tidelift.yml vendored Normal file
View 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
View File

@ -83,6 +83,7 @@ docs/_build/
Tests/images/README.md Tests/images/README.md
Tests/images/crash_1.tif Tests/images/crash_1.tif
Tests/images/crash_2.tif Tests/images/crash_2.tif
Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif
Tests/images/string_dimension.tiff Tests/images/string_dimension.tiff
Tests/images/jpeg2000 Tests/images/jpeg2000
Tests/images/msp Tests/images/msp

View File

@ -1,43 +1,43 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: e3000ace2fd1fcb1c181bb7a8285f1f976bcbdc7 # frozen: 21.7b0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
args: ["--target-version", "py36"] args: ["--target-version", "py37"]
# Only .py files, until https://github.com/psf/black/issues/402 resolved # Only .py files, until https://github.com/psf/black/issues/402 resolved
files: \.py$ files: \.py$
types: [] types: []
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: fd5ba70665a37ec301a1f714ed09336048b3be63 # frozen: 5.9.3 rev: 5.10.1
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/asottile/yesqa - repo: https://github.com/asottile/yesqa
rev: 644ede78511c02fc6f8e03e014cc1ddcfbf1e1f5 # frozen: v1.2.3 rev: v1.3.0
hooks: hooks:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10 rev: v1.1.13
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://gitlab.com/pycqa/flake8 - repo: https://github.com/PyCQA/flake8
rev: dcd740bc0ebaf2b3d43e59a0060d157c97de13f3 # frozen: 3.9.2 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat] additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0 rev: v1.9.0
hooks: hooks:
- id: python-check-blanket-noqa - id: python-check-blanket-noqa
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: 38b88246ccc552bffaaf54259d064beeee434539 # frozen: v4.0.1 rev: v4.1.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-yaml - id: check-yaml

View File

@ -1,2 +1,8 @@
version: 2
python: python:
pip_install: true install:
- method: pip
path: .
extra_requirements:
- docs

View File

@ -2,9 +2,300 @@
Changelog (Pillow) 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 - Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655
[radarhere] [radarhere]
@ -59,12 +350,30 @@ Changelog (Pillow)
- Fixed ImageOps expand with tuple border on P image #5615 - Fixed ImageOps expand with tuple border on P image #5615
[radarhere] [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 - Fixed error saving APNG with duplicate frames and different duration times #5609
[thak1411, radarhere] [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) 8.3.1 (2021-07-06)
------------------ ------------------
@ -338,7 +647,7 @@ Changelog (Pillow)
- Changed Image.open formats parameter to be case-insensitive #5250 - Changed Image.open formats parameter to be case-insensitive #5250
[Piolie, radarhere] [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] [radarhere]
- Added tk version to pilinfo #5226 - Added tk version to pilinfo #5226

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is Pillow is the friendly PIL fork. It is
Copyright © 2010-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: Like PIL, Pillow is licensed under the open source HPND License:

View File

@ -1,6 +1,7 @@
include *.c include *.c
include *.h include *.h
include *.in include *.in
include *.lock
include *.md include *.md
include *.py include *.py
include *.rst include *.rst
@ -9,6 +10,7 @@ include *.txt
include *.yaml include *.yaml
include LICENSE include LICENSE
include Makefile include Makefile
include Pipfile
include tox.ini include tox.ini
graft Tests graft Tests
graft src graft src

View File

@ -9,9 +9,11 @@ clean:
.PHONY: coverage .PHONY: coverage
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 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 .PHONY: doc
doc: doc:
@ -33,33 +35,29 @@ help:
@echo "Welcome to Pillow development. Please use \`make <target>\` where <target> is one of" @echo "Welcome to Pillow development. Please use \`make <target>\` where <target> is one of"
@echo " clean remove build products" @echo " clean remove build products"
@echo " coverage run coverage test (in progress)" @echo " coverage run coverage test (in progress)"
@echo " doc make html docs" @echo " doc make HTML docs"
@echo " docserve run an http server on the docs directory" @echo " docserve run an HTTP server on the docs directory"
@echo " html to make standalone HTML files" @echo " html to make standalone HTML files"
@echo " inplace make inplace extension" @echo " inplace make inplace extension"
@echo " install make and install" @echo " install make and install"
@echo " install-coverage make and install with C coverage" @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 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 " release-test run code and package tests before release"
@echo " test run tests on installed pillow" @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"
.PHONY: inplace .PHONY: inplace
inplace: clean inplace: clean
python3 setup.py develop build_ext --inplace python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" .
.PHONY: install .PHONY: install
install: install:
python3 setup.py install python3 -m pip install .
python3 selftest.py python3 selftest.py
.PHONY: install-coverage .PHONY: install-coverage
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 python3 selftest.py
.PHONY: debug .PHONY: debug
@ -68,58 +66,52 @@ debug:
# for our stuff, kills optimization, and redirects to dev null so we # for our stuff, kills optimization, and redirects to dev null so we
# see any build failures. # see any build failures.
make clean > /dev/null make clean > /dev/null
CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /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
.PHONY: release-test .PHONY: release-test
release-test: release-test:
$(MAKE) install-req python3 -m pip install -e .[tests]
python3 setup.py develop
python3 selftest.py python3 selftest.py
python3 -m pytest Tests python3 -m pytest Tests
python3 setup.py install python3 -m pip install .
-rm dist/*.egg -rm dist/*.egg
-rmdir dist -rmdir dist
python3 -m pytest -qq python3 -m pytest -qq
check-manifest python3 -m check_manifest
pyroma . python3 -m pyroma .
$(MAKE) readme $(MAKE) readme
.PHONY: sdist .PHONY: sdist
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 .PHONY: test
test: test:
pytest -qq python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq
.PHONY: valgrind .PHONY: valgrind
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 \ PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/tmp/valgrind-output \ --log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme .PHONY: readme
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 .PHONY: lint
lint: lint:
tox --help > /dev/null || python3 -m pip install tox python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
tox -e lint python3 -m tox -e lint
.PHONY: lint-fix .PHONY: lint-fix
lint-fix: lint-fix:
black --target-version py36 . python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
isort . 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
View 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
View 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": {}
}

View File

@ -1,5 +1,5 @@
<p align="center"> <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> </p>
# Pillow # Pillow
@ -24,30 +24,36 @@ As of 2019, Pillow development is
<tr> <tr>
<th>tests</th> <th>tests</th>
<td> <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)" alt="GitHub Actions build status (Lint)"
src="https://github.com/python-pillow/Pillow/workflows/Lint/badge.svg"></a> 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)" alt="GitHub Actions build status (Test Linux and macOS)"
src="https://github.com/python-pillow/Pillow/workflows/Test/badge.svg"></a> 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)" alt="GitHub Actions build status (Test Windows)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Windows/badge.svg"></a> 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)" alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img <a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
alt="AppVeyor CI build status (Windows)" 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 <a href="https://github.com/python-pillow/pillow-wheels/actions"><img
alt="GitHub Actions wheels build status (Wheels)" alt="GitHub Actions wheels build status (Wheels)"
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a> 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)" alt="Travis CI wheels build status (aarch64)"
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/master.svg?label=aarch64%20wheels"></a> src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels"></a>
<a href="https://codecov.io/gh/python-pillow/Pillow"><img <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/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> </td>
</tr> </tr>
<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/) - [Documentation](https://pillow.readthedocs.io/)
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html) - [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.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) - [Issues](https://github.com/python-pillow/Pillow/issues)
- [Pull requests](https://github.com/python-pillow/Pillow/pulls) - [Pull requests](https://github.com/python-pillow/Pillow/pulls)
- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork) - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
## Report a Vulnerability ## Report a Vulnerability

View File

@ -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. Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `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 `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 `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. * [ ] 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` * [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`. * [ ] Update `CHANGES.rst`.
@ -24,13 +24,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create and check source distribution: * [ ] Create and check source distribution:
```bash ```bash
make sdist 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.: * [ ] Check and upload all binaries and source distributions e.g.:
```bash ```bash
twine check dist/* python3 -m twine check --strict dist/*
twine upload dist/Pillow-5.2.0* python3 -m twine upload dist/Pillow-5.2.0*
``` ```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` * [ ] 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. 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`. * [ ] Update `CHANGES.rst`.
* [ ] Check out release branch e.g.: * [ ] Check out release branch e.g.:
```bash ```bash
git checkout -t remotes/origin/5.2.x 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist 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.: * [ ] Check and upload all binaries and source distributions e.g.:
```bash ```bash
twine check dist/* python3 -m twine check --strict dist/*
twine upload dist/Pillow-5.2.1* python3 -m twine upload dist/Pillow-5.2.1*
``` ```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] 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. 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. * [ ] 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. * [ ] Run local test matrix on each release & Python version.
* [ ] Privately send to distros. * [ ] Privately send to distros.
* [ ] Run pre-release check via `make release-test` * [ ] 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist 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) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
## Binary Distributions ## Binary Distributions

View File

@ -4,5 +4,5 @@ import sys
from PIL import Image from PIL import Image
if sys.maxsize < 2 ** 32: if sys.maxsize < 2**32:
im = Image.new("L", (999999, 999999), 0) im = Image.new("L", (999999, 999999), 0)

View File

@ -61,8 +61,8 @@ repro_copy = (
for path in repro_ss2 + repro_lc + repro_advance + repro_brun + 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: try:
im.load() im.load()
except Exception as msg: except Exception as msg:
print(msg) print(msg)

View File

@ -19,8 +19,8 @@ from PIL import Image
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")
for path in repro: for path in repro:
im = Image.open(path) with Image.open(path) as im:
try: try:
im.load() im.load()
except Exception as msg: except Exception as msg:
print(msg) print(msg)

View File

@ -23,7 +23,7 @@ YDIM = 32769
XDIM = 48000 XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path, xdim, ydim):

View File

@ -19,7 +19,7 @@ YDIM = 32769
XDIM = 48000 XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path, xdim, ydim):

View File

@ -30,7 +30,6 @@ if os.environ.get("SHOW_ERRORS", None):
a.show() a.show()
b.show() b.show()
elif "GITHUB_ACTIONS" in os.environ: elif "GITHUB_ACTIONS" in os.environ:
HAS_UPLOADER = True HAS_UPLOADER = True
@ -44,7 +43,6 @@ elif "GITHUB_ACTIONS" in os.environ:
b.save(os.path.join(tmpdir, "b.png")) b.save(os.path.join(tmpdir, "b.png"))
return tmpdir return tmpdir
else: else:
try: try:
import test_image_results import test_image_results
@ -326,7 +324,7 @@ def is_mingw():
return sysconfig.get_platform() == "mingw" return sysconfig.get_platform() == "mingw"
class cached_property: class CachedProperty:
def __init__(self, func): def __init__(self, func):
self.func = func self.func = func

BIN
Tests/images/16bit.r.tif Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
Tests/images/no_palette.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -22,7 +22,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
fuzzer_basename=$(basename -s .py $fuzzer) fuzzer_basename=$(basename -s .py $fuzzer)
fuzzer_package=${fuzzer_basename}.pkg fuzzer_package=${fuzzer_basename}.pkg
pyinstaller \ 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/libfreetype.so.6:. \
--add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \
--add-binary /usr/local/lib/libopenjp2.so.7:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \

View File

@ -14,10 +14,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import atheris_no_libfuzzer as atheris import atheris
import fuzzers
with atheris.instrument_imports():
import sys
import fuzzers
def TestOneInput(data): def TestOneInput(data):
@ -26,13 +29,12 @@ def TestOneInput(data):
except Exception: except Exception:
# We're catching all exceptions because Pillow's exceptions are # We're catching all exceptions because Pillow's exceptions are
# directly inheriting from Exception. # directly inheriting from Exception.
return pass
return
def main(): def main():
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()
fuzzers.disable_decompressionbomb_error() fuzzers.disable_decompressionbomb_error()

View File

@ -14,10 +14,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys
import atheris_no_libfuzzer as atheris import atheris
import fuzzers
with atheris.instrument_imports():
import sys
import fuzzers
def TestOneInput(data): def TestOneInput(data):
@ -26,13 +29,12 @@ def TestOneInput(data):
except Exception: except Exception:
# We're catching all exceptions because Pillow's exceptions are # We're catching all exceptions because Pillow's exceptions are
# directly inheriting from Exception. # directly inheriting from Exception.
return pass
return
def main(): def main():
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()
fuzzers.disable_decompressionbomb_error() fuzzers.disable_decompressionbomb_error()

View File

@ -1,6 +1,5 @@
import os import os
import warnings
import pytest
from PIL import Image from PIL import Image
@ -20,16 +19,14 @@ def test_bad():
either""" either"""
for f in get_files("b"): 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: try:
with Image.open(f) as im: with Image.open(f) as im:
im.load() im.load()
except Exception: # as msg: except Exception: # as msg:
pass pass
# Assert that there is no unclosed file warning
assert not record
def test_questionable(): def test_questionable():
"""These shouldn't crash/dos, but it's not well defined that these """These shouldn't crash/dos, but it's not well defined that these
@ -43,6 +40,7 @@ def test_questionable():
"rgb32fakealpha.bmp", "rgb32fakealpha.bmp",
"rgb24largepal.bmp", "rgb24largepal.bmp",
"pal8os2sp.bmp", "pal8os2sp.bmp",
"pal8rletrns.bmp",
"rgb32bf-xbgr.bmp", "rgb32bf-xbgr.bmp",
] ]
for f in get_files("q"): for f in get_files("q"):

View File

@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1):
return image._new(image.im.box_blur(radius, n)) 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()) it = iter(im.getdata())
for data_row in data: for data_row in data:
im_row = [next(it) for _ in range(im.size[0])] im_row = [next(it) for _ in range(im.size[0])]
@ -35,12 +35,12 @@ def assertImage(im, data, delta=0):
next(it) next(it)
def assertBlur(im, radius, data, passes=1, delta=0): def assert_blur(im, radius, data, passes=1, delta=0):
# check grayscale image # 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)) rgba = Image.merge("RGBA", (im, im, im, im))
for band in box_blur(rgba, radius, passes).split(): for band in box_blur(rgba, radius, passes).split():
assertImage(band, data, delta) assert_image(band, data, delta)
def test_color_modes(): def test_color_modes():
@ -64,7 +64,7 @@ def test_color_modes():
def test_radius_0(): def test_radius_0():
assertBlur( assert_blur(
sample, sample,
0, 0,
[ [
@ -80,7 +80,7 @@ def test_radius_0():
def test_radius_0_02(): def test_radius_0_02():
assertBlur( assert_blur(
sample, sample,
0.02, 0.02,
[ [
@ -97,7 +97,7 @@ def test_radius_0_02():
def test_radius_0_05(): def test_radius_0_05():
assertBlur( assert_blur(
sample, sample,
0.05, 0.05,
[ [
@ -114,7 +114,7 @@ def test_radius_0_05():
def test_radius_0_1(): def test_radius_0_1():
assertBlur( assert_blur(
sample, sample,
0.1, 0.1,
[ [
@ -131,7 +131,7 @@ def test_radius_0_1():
def test_radius_0_5(): def test_radius_0_5():
assertBlur( assert_blur(
sample, sample,
0.5, 0.5,
[ [
@ -148,7 +148,7 @@ def test_radius_0_5():
def test_radius_1(): def test_radius_1():
assertBlur( assert_blur(
sample, sample,
1, 1,
[ [
@ -165,7 +165,7 @@ def test_radius_1():
def test_radius_1_5(): def test_radius_1_5():
assertBlur( assert_blur(
sample, sample,
1.5, 1.5,
[ [
@ -182,7 +182,7 @@ def test_radius_1_5():
def test_radius_bigger_then_half(): def test_radius_bigger_then_half():
assertBlur( assert_blur(
sample, sample,
3, 3,
[ [
@ -199,7 +199,7 @@ def test_radius_bigger_then_half():
def test_radius_bigger_then_width(): def test_radius_bigger_then_width():
assertBlur( assert_blur(
sample, sample,
10, 10,
[ [
@ -214,7 +214,7 @@ def test_radius_bigger_then_width():
def test_extreme_large_radius(): def test_extreme_large_radius():
assertBlur( assert_blur(
sample, sample,
600, 600,
[ [
@ -229,7 +229,7 @@ def test_extreme_large_radius():
def test_two_passes(): def test_two_passes():
assertBlur( assert_blur(
sample, sample,
1, 1,
[ [
@ -247,7 +247,7 @@ def test_two_passes():
def test_three_passes(): def test_three_passes():
assertBlur( assert_blur(
sample, sample,
1, 1,
[ [

View File

@ -15,27 +15,27 @@ except ImportError:
class TestColorLut3DCoreAPI: class TestColorLut3DCoreAPI:
def generate_identity_table(self, channels, size): def generate_identity_table(self, channels, size):
if isinstance(size, tuple): if isinstance(size, tuple):
size1D, size2D, size3D = size size_1d, size_2d, size_3d = size
else: else:
size1D, size2D, size3D = (size, size, size) size_1d, size_2d, size_3d = (size, size, size)
table = [ table = [
[ [
r / (size1D - 1) if size1D != 1 else 0, r / (size_1d - 1) if size_1d != 1 else 0,
g / (size2D - 1) if size2D != 1 else 0, g / (size_2d - 1) if size_2d != 1 else 0,
b / (size3D - 1) if size3D != 1 else 0, b / (size_3d - 1) if size_3d != 1 else 0,
r / (size1D - 1) if size1D != 1 else 0, r / (size_1d - 1) if size_1d != 1 else 0,
g / (size2D - 1) if size2D != 1 else 0, g / (size_2d - 1) if size_2d != 1 else 0,
][:channels] ][:channels]
for b in range(size3D) for b in range(size_3d)
for g in range(size2D) for g in range(size_2d)
for r in range(size1D) for r in range(size_1d)
] ]
return ( return (
channels, channels,
size1D, size_1d,
size2D, size_2d,
size3D, size_3d,
[item for sublist in table for item in sublist], [item for sublist in table for item in sublist],
) )
@ -43,107 +43,158 @@ class TestColorLut3DCoreAPI:
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
with pytest.raises(ValueError, match="filter"): with pytest.raises(ValueError, match="filter"):
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"): with pytest.raises(ValueError, match="image mode"):
im.im.color_lut_3d( 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"): 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( 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"): with pytest.raises(ValueError, match="Table size"):
im.im.color_lut_3d( 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"): 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"): 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): 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): 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): def test_correct_args(self):
im = Image.new("RGB", (10, 10), 0) 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( 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( 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( 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( 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): def test_wrong_mode(self):
with pytest.raises(ValueError, match="wrong mode"): with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0) im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d("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( 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"): with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0) 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): def test_correct_mode(self):
im = Image.new("RGBA", (10, 10), 0) 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 = 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 = 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 = 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): def test_identities(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( 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 # Fast test with small cubes
@ -152,7 +203,9 @@ class TestColorLut3DCoreAPI:
im, im,
im._new( im._new(
im.im.color_lut_3d( 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,
im._new( im._new(
im.im.color_lut_3d( 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): def test_identities_4_channels(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( 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 # Red channel copied to alpha
@ -178,7 +238,9 @@ class TestColorLut3DCoreAPI:
Image.merge("RGBA", (im.split() * 2)[:4]), Image.merge("RGBA", (im.split() * 2)[:4]),
im._new( im._new(
im.im.color_lut_3d( 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", "RGBA",
[ [
g, g,
g.transpose(Image.ROTATE_90), g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.ROTATE_180), g.transpose(Image.Transpose.ROTATE_180),
g.transpose(Image.ROTATE_270), g.transpose(Image.Transpose.ROTATE_270),
], ],
) )
@ -199,7 +261,9 @@ class TestColorLut3DCoreAPI:
im, im,
im._new( im._new(
im.im.color_lut_3d( 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): def test_channels_order(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( 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 # Reverse channels by splitting and using table
# fmt: off # fmt: off
assert_image_equal( assert_image_equal(
Image.merge('RGB', im.split()[::-1]), 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, [ 3, 2, 2, 2, [
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1,
@ -227,11 +296,16 @@ class TestColorLut3DCoreAPI:
def test_overflow(self): def test_overflow(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( 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 # 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, 2, 2, 2,
[ [
-1, -1, -1, 2, -1, -1, -1, -1, -1, 2, -1, -1,
@ -251,7 +325,7 @@ class TestColorLut3DCoreAPI:
assert transformed[205, 205] == (255, 255, 0) assert transformed[205, 205] == (255, 255, 0)
# fmt: off # 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, 2, 2, 2,
[ [
-3, -3, -3, 5, -3, -3, -3, -3, -3, 5, -3, -3,
@ -354,7 +428,12 @@ class TestColorLut3DFilter:
def test_numpy_formats(self): def test_numpy_formats(self):
g = Image.linear_gradient("L") g = Image.linear_gradient("L")
im = Image.merge( 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)) 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") g = Image.linear_gradient("L")
im = Image.merge( 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) assert im == im.filter(lut)

View File

@ -110,9 +110,9 @@ class TestCoreMemory:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.core.set_blocks_max(-1) Image.core.set_blocks_max(-1)
if sys.maxsize < 2 ** 32: if sys.maxsize < 2**32:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.core.set_blocks_max(2 ** 29) Image.core.set_blocks_max(2**29)
@pytest.mark.skipif(is_pypy(), reason="Images not collected") @pytest.mark.skipif(is_pypy(), reason="Images not collected")
def test_set_blocks_max_stats(self): def test_set_blocks_max_stats(self):

View File

@ -78,7 +78,7 @@ class TestDecompressionCrop:
def teardown_class(self): def teardown_class(self):
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def testEnlargeCrop(self): def test_enlarge_crop(self):
# Crops can extend the extents, therefore we should have the # Crops can extend the extents, therefore we should have the
# same decompression bomb warnings on them. # same decompression bomb warnings on them.
with hopper() as src: with hopper() as src:
@ -86,21 +86,12 @@ class TestDecompressionCrop:
pytest.warns(Image.DecompressionBombWarning, src.crop, box) pytest.warns(Image.DecompressionBombWarning, src.crop, box)
def test_crop_decompression_checks(self): def test_crop_decompression_checks(self):
im = Image.new("RGB", (100, 100)) im = Image.new("RGB", (100, 100))
good_values = ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)) for value in ((-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:
assert im.crop(value).size == (9, 9) assert im.crop(value).size == (9, 9)
for value in warning_values: pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99))
pytest.warns(Image.DecompressionBombWarning, im.crop, value)
for value in error_values: with pytest.raises(Image.DecompressionBombError):
with pytest.raises(Image.DecompressionBombError): im.crop((-99909, -99990, 99999, 99999))
im.crop(value)

91
Tests/test_deprecate.py Normal file
View 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)

View File

@ -120,9 +120,9 @@ def test_apng_dispose_op_previous_frame():
# save_all=True, # save_all=True,
# append_images=[green, blue], # append_images=[green, blue],
# disposal=[ # disposal=[
# PngImagePlugin.APNG_DISPOSE_OP_NONE, # PngImagePlugin.Disposal.OP_NONE,
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, # PngImagePlugin.Disposal.OP_PREVIOUS,
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS # PngImagePlugin.Disposal.OP_PREVIOUS
# ], # ],
# ) # )
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: 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.n_frames == 1
assert im.info.get("duration") == 750 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): def test_apng_save_disposal(tmp_path):
test_file = str(tmp_path / "temp.png") 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)) green = Image.new("RGBA", size, (0, 255, 0, 255))
transparent = Image.new("RGBA", size, (0, 0, 0, 0)) transparent = Image.new("RGBA", size, (0, 0, 0, 0))
# test APNG_DISPOSE_OP_NONE # test OP_NONE
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green, transparent], append_images=[green, transparent],
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(2) im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_DISPOSE_OP_BACKGROUND # test OP_BACKGROUND
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, PngImagePlugin.Disposal.OP_BACKGROUND,
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, transparent], append_images=[red, transparent],
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(2) im.seek(2)
@ -481,26 +487,26 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, PngImagePlugin.Disposal.OP_BACKGROUND,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green], append_images=[green],
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(1) im.seek(1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_DISPOSE_OP_PREVIOUS # test OP_PREVIOUS
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, PngImagePlugin.Disposal.OP_PREVIOUS,
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
] ]
red.save( red.save(
test_file, test_file,
@ -508,7 +514,7 @@ def test_apng_save_disposal(tmp_path):
append_images=[green, red, transparent], append_images=[green, red, transparent],
default_image=True, default_image=True,
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(3) im.seek(3)
@ -516,21 +522,32 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
disposal = [ disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE, PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, PngImagePlugin.Disposal.OP_PREVIOUS,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green], append_images=[green],
disposal=disposal, disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(1) im.seek(1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# 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): def test_apng_save_disposal_previous(tmp_path):
test_file = str(tmp_path / "temp.png") 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)) red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
# test APNG_DISPOSE_OP_NONE # test OP_NONE
transparent.save( transparent.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, green], append_images=[red, green],
disposal=PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(2) im.seek(2)
@ -559,17 +576,17 @@ def test_apng_save_blend(tmp_path):
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
transparent = Image.new("RGBA", size, (0, 0, 0, 0)) transparent = Image.new("RGBA", size, (0, 0, 0, 0))
# test APNG_BLEND_OP_SOURCE on solid color # test OP_SOURCE on solid color
blend = [ blend = [
PngImagePlugin.APNG_BLEND_OP_OVER, PngImagePlugin.Blend.OP_OVER,
PngImagePlugin.APNG_BLEND_OP_SOURCE, PngImagePlugin.Blend.OP_SOURCE,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, green], append_images=[red, green],
default_image=True, default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=blend, blend=blend,
) )
with Image.open(test_file) as im: 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((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (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 = [ blend = [
PngImagePlugin.APNG_BLEND_OP_OVER, PngImagePlugin.Blend.OP_OVER,
PngImagePlugin.APNG_BLEND_OP_SOURCE, PngImagePlugin.Blend.OP_SOURCE,
] ]
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, transparent], append_images=[red, transparent],
default_image=True, default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=blend, blend=blend,
) )
with Image.open(test_file) as im: 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((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0)
# test APNG_BLEND_OP_OVER # test OP_OVER
red.save( red.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[green, transparent], append_images=[green, transparent],
default_image=True, default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, disposal=PngImagePlugin.Disposal.OP_NONE,
blend=PngImagePlugin.APNG_BLEND_OP_OVER, blend=PngImagePlugin.Blend.OP_OVER,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.seek(1) im.seek(1)
@ -611,3 +628,20 @@ def test_apng_save_blend(tmp_path):
im.seek(2) im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# 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]

View File

@ -1,8 +1,18 @@
import pytest 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(): 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") 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( @pytest.mark.parametrize(
"test_file", "test_file",
[ [
@ -37,3 +69,14 @@ def test_crashes(test_file):
with Image.open(f) as im: with Image.open(f) as im:
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() 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]

View File

@ -4,7 +4,12 @@ import pytest
from PIL import BmpImagePlugin, Image 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): def test_sanity(tmp_path):
@ -123,3 +128,46 @@ def test_rgba_bitfields():
im = Image.merge("RGB", (r, g, b)) im = Image.merge("RGB", (r, g, b))
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") 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")

View File

@ -45,3 +45,35 @@ def test_save(tmp_path):
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(tmpfile) 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

View File

@ -1,3 +1,5 @@
import warnings
import pytest import pytest
from PIL import DcxImagePlugin, Image from PIL import DcxImagePlugin, Image
@ -31,21 +33,17 @@ def test_unclosed_file():
def test_closed_file(): def test_closed_file():
with pytest.warns(None) as record: with warnings.catch_warnings():
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
im.close() im.close()
assert not record
def test_context_manager(): def test_context_manager():
with pytest.warns(None) as record: with warnings.catch_warnings():
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
assert not record
def test_invalid_file(): def test_invalid_file():
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:

View File

@ -196,6 +196,13 @@ def test__accept_false():
assert not output 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(): def test_short_header():
"""Check a short header""" """Check a short header"""
with open(TEST_FILE_DXT5, "rb") as f: with open(TEST_FILE_DXT5, "rb") as f:

View File

@ -58,6 +58,15 @@ def test_sanity():
assert image2_scale2.format == "EPS" 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(): def test_invalid_file():
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"

80
Tests/test_file_fits.py Normal file
View 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,
)

View File

@ -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)

View File

@ -1,3 +1,5 @@
import warnings
import pytest import pytest
from PIL import FliImagePlugin, Image from PIL import FliImagePlugin, Image
@ -38,21 +40,17 @@ def test_unclosed_file():
def test_closed_file(): def test_closed_file():
with pytest.warns(None) as record: with warnings.catch_warnings():
im = Image.open(static_test_file) im = Image.open(static_test_file)
im.load() im.load()
im.close() im.close()
assert not record
def test_context_manager(): def test_context_manager():
with pytest.warns(None) as record: with warnings.catch_warnings():
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
im.load() im.load()
assert not record
def test_tell(): def test_tell():
# Arrange # Arrange
@ -138,3 +136,16 @@ def test_timeouts(test_file):
with Image.open(f) as im: with Image.open(f) as im:
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() 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()

View File

@ -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 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.ftc") as im:
with Image.open("Tests/images/ftex_dxt1.png") as target: with Image.open("Tests/images/ftex_dxt1.png") as target:
assert_image_similar(im, target.convert("RGBA"), 15) assert_image_similar(im, target.convert("RGBA"), 15)
def test_invalid_file():
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]

View File

@ -5,20 +5,28 @@ from PIL import GbrImagePlugin, Image
from .helper import assert_image_equal_tofile 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(): def test_gbr_file():
with Image.open("Tests/images/gbr.gbr") as im: with Image.open("Tests/images/gbr.gbr") as im:
assert_image_equal_tofile(im, "Tests/images/gbr.png") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_load():
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(): def test_multiple_load_operations():
with Image.open("Tests/images/gbr.gbr") as im: with Image.open("Tests/images/gbr.gbr") as im:
im.load() im.load()
im.load() im.load()
assert_image_equal_tofile(im, "Tests/images/gbr.png") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
GbrImagePlugin.GbrImageFile(invalid_file)

View File

@ -1,3 +1,4 @@
import warnings
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -39,21 +40,17 @@ def test_unclosed_file():
def test_closed_file(): def test_closed_file():
with pytest.warns(None) as record: with warnings.catch_warnings():
im = Image.open(TEST_GIF) im = Image.open(TEST_GIF)
im.load() im.load()
im.close() im.close()
assert not record
def test_context_manager(): def test_context_manager():
with pytest.warns(None) as record: with warnings.catch_warnings():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im.load() im.load()
assert not record
def test_invalid_file(): def test_invalid_file():
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -62,6 +59,51 @@ def test_invalid_file():
GifImagePlugin.GifImageFile(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_optimize():
def test_grayscale(optimize): def test_grayscale(optimize):
im = Image.new("L", (1, 1), 0) im = Image.new("L", (1, 1), 0)
@ -163,6 +205,32 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5 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): def test_headers_saving_for_animated_gifs(tmp_path):
important_headers = ["background", "version", "duration", "loop"] important_headers = ["background", "version", "duration", "loop"]
# Multiframe image # Multiframe image
@ -184,8 +252,8 @@ def test_palette_handling(tmp_path):
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
im = im.convert("RGB") im = im.convert("RGB")
im = im.resize((100, 100), Image.LANCZOS) im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
f = str(tmp_path / "temp.gif") f = str(tmp_path / "temp.gif")
im2.save(f, optimize=True) im2.save(f, optimize=True)
@ -285,6 +353,22 @@ def test_n_frames():
assert im.is_animated == (n_frames != 1) 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(): def test_eoferror():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
n_frames = im.n_frames 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: with Image.open("Tests/images/dispose_none_load_end.gif") as img:
img.seek(1) 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(): def test_dispose_background():
@ -337,14 +421,45 @@ def test_dispose_background():
pass pass
def test_transparent_dispose(): def test_dispose_background_transparency():
expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)] with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
with Image.open("Tests/images/transparent_dispose.gif") as img: img.seek(2)
for frame in range(3): px = img.load()
img.seek(frame) assert px[35, 30][3] == 0
for x in range(3):
color = img.getpixel((x, 0))
assert color == expected_colors[frame][x] @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(): 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: with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
im.seek(1) im.seek(1)
assert_image_equal_tofile( assert_image_equal_tofile(
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: with Image.open(out) as im:
im.seek(1) im.seek(1)
assert im.getpixel((0, 0)) == 0 assert im.getpixel((0, 0)) == (255, 0, 0)
def test_transparency_in_second_frame(): def test_transparency_in_second_frame():
@ -510,9 +625,9 @@ def test_transparency_in_second_frame():
# Seek to the second frame # Seek to the second frame
im.seek(im.tell() + 1) 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(): def test_no_transparency_in_second_frame():
@ -684,31 +799,31 @@ def test_zero_comment_subblocks():
def test_version(tmp_path): def test_version(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
def assertVersionAfterSave(im, version): def assert_version_after_save(im, version):
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["version"] == version assert reread.info["version"] == version
# Test that GIF87a is used by default # Test that GIF87a is used by default
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
assertVersionAfterSave(im, b"GIF87a") assert_version_after_save(im, b"GIF87a")
# Test setting the version to 89a # Test setting the version to 89a
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.info["version"] = b"89a" 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 # Test that adding a GIF89a feature changes the version
im.info["transparency"] = 1 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 # Test that a GIF87a image is also saved in that format
with Image.open("Tests/images/test.colors.gif") as im: 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 # Test that a GIF89a image is also saved in that format
im.info["version"] = b"GIF89a" im.info["version"] = b"GIF89a"
assertVersionAfterSave(im, b"GIF87a") assert_version_after_save(im, b"GIF87a")
def test_append_images(tmp_path): def test_append_images(tmp_path):
@ -723,10 +838,10 @@ def test_append_images(tmp_path):
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending using a generator # Tests appending using a generator
def imGenerator(ims): def im_generator(ims):
yield from 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: with Image.open(out) as reread:
assert reread.n_frames == 3 assert reread.n_frames == 3
@ -781,6 +896,17 @@ def test_rgb_transparency(tmp_path):
assert "transparency" not in reloaded.info 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): def test_bbox(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -811,7 +937,7 @@ def test_palette_save_P(tmp_path):
# Forcing a non-straight grayscale palette. # Forcing a non-straight grayscale palette.
im = hopper("P") 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") out = str(tmp_path / "temp.gif")
im.save(out, palette=palette) im.save(out, palette=palette)
@ -856,7 +982,7 @@ def test_palette_save_ImagePalette(tmp_path):
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
im.putpalette(palette) im.putpalette(palette)
assert_image_equal(reloaded, im) assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
def test_save_I(tmp_path): def test_save_I(tmp_path):
@ -874,11 +1000,11 @@ def test_save_I(tmp_path):
def test_getdata(): def test_getdata():
# Test getheader/getdata against legacy values. # Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette. # Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.NEAREST) im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
im.putpalette(ImagePalette.ImagePalette("RGB")) im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0} 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 GifImagePlugin._FORCE_OPTIMIZE = True
try: try:
@ -910,6 +1036,11 @@ def test_lzw_bits():
def test_extents(): def test_extents():
with Image.open("Tests/images/test_extents.gif") as im: with Image.open("Tests/images/test_extents.gif") as im:
assert im.size == (100, 100) assert im.size == (100, 100)
# Check that n_frames does not change the size
assert im.n_frames == 2
assert im.size == (100, 100)
im.seek(1) im.seek(1)
assert im.size == (150, 150) assert im.size == (150, 150)
@ -919,4 +1050,14 @@ def test_missing_background():
# but the disposal method is "Restore to background color" # but the disposal method is "Restore to background color"
with Image.open("Tests/images/missing_background.gif") as im: with Image.open("Tests/images/missing_background.gif") as im:
im.seek(1) 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

View File

@ -45,3 +45,35 @@ def test_save(tmp_path):
# Act / Assert: stub cannot save without an implemented handler # Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(tmpfile) 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

View File

@ -46,3 +46,35 @@ def test_save():
im.save(dummy_filename) im.save(dummy_filename)
with pytest.raises(OSError): with pytest.raises(OSError):
Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) 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

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