Merge branch 'main' into main
|
@ -13,34 +13,34 @@ environment:
|
||||||
- PYTHON: C:/Python311
|
- PYTHON: C:/Python311
|
||||||
ARCHITECTURE: x86
|
ARCHITECTURE: x86
|
||||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||||
- PYTHON: C:/Python37-x64
|
- PYTHON: C:/Python38-x64
|
||||||
ARCHITECTURE: x64
|
ARCHITECTURE: x64
|
||||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||||
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- '%PYTHON%\%EXECUTABLE% --version'
|
- '%PYTHON%\%EXECUTABLE% --version'
|
||||||
|
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
|
||||||
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||||
- 7z x pillow-depends.zip -oc:\
|
- 7z x pillow-depends.zip -oc:\
|
||||||
- 7z x pillow-test-images.zip -oc:\
|
- 7z x pillow-test-images.zip -oc:\
|
||||||
- mv c:\pillow-depends-main c:\pillow-depends
|
- mv c:\pillow-depends-main c:\pillow-depends
|
||||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
|
- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\
|
||||||
- ..\pillow-depends\gs1000w32.exe /S
|
- choco install ghostscript --version=10.0.0.20230317
|
||||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH%
|
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||||
- cd c:\pillow\winbuild\
|
- cd c:\pillow\winbuild\
|
||||||
- ps: |
|
- ps: |
|
||||||
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
c:\python38\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%
|
||||||
|
|
||||||
build_script:
|
build_script:
|
||||||
- ps: |
|
|
||||||
c:\pillow\winbuild\build\build_pillow.cmd install
|
|
||||||
$host.SetShouldExit(0)
|
|
||||||
- cd c:\pillow
|
- cd c:\pillow
|
||||||
|
- winbuild\build\build_env.cmd
|
||||||
|
- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .'
|
||||||
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
|
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
|
@ -52,8 +52,8 @@ test_script:
|
||||||
#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest?
|
#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest?
|
||||||
|
|
||||||
after_test:
|
after_test:
|
||||||
- python -m pip install codecov
|
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe
|
||||||
- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor
|
- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
@ -62,18 +62,15 @@ cache:
|
||||||
- '%LOCALAPPDATA%\pip\Cache'
|
- '%LOCALAPPDATA%\pip\Cache'
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
- path: pillow\dist\*.egg
|
- path: pillow\*.egg
|
||||||
name: egg
|
name: egg
|
||||||
- path: pillow\dist\*.wheel
|
- path: pillow\*.whl
|
||||||
name: wheel
|
name: wheel
|
||||||
|
|
||||||
before_deploy:
|
before_deploy:
|
||||||
- cd c:\pillow
|
- cd c:\pillow
|
||||||
- '%PYTHON%\%EXECUTABLE% -m pip install wheel'
|
- '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .'
|
||||||
- cd c:\pillow\winbuild\
|
- ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
|
||||||
- c:\pillow\winbuild\build\build_pillow.cmd bdist_wheel
|
|
||||||
- cd c:\pillow
|
|
||||||
- ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: S3
|
provider: S3
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# gather the coverage data
|
# gather the coverage data
|
||||||
python3 -m pip install codecov
|
python3 -m pip install coverage
|
||||||
if [[ $MATRIX_DOCKER ]]; then
|
if [[ $MATRIX_DOCKER ]]; then
|
||||||
python3 -m coverage xml --ignore-errors
|
python3 -m coverage xml --ignore-errors
|
||||||
else
|
else
|
||||||
|
|
|
@ -22,7 +22,8 @@ set -e
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
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 meson imagemagick libharfbuzz-dev libfribidi-dev
|
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||||
|
sway wl-clipboard
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
|
@ -42,7 +43,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
|
|
||||||
# PyQt6 doesn't support PyPy3
|
# PyQt6 doesn't support PyPy3
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
||||||
python3 -m pip install pyqt6
|
python3 -m pip install pyqt6
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,6 @@ indent_style = space
|
||||||
|
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.rst]
|
|
||||||
# Four-space indentation
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
[*.yml]
|
[*.yml]
|
||||||
# Two-space indentation
|
# Two-space indentation
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
1
.github/CONTRIBUTING.md
vendored
|
@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
|
||||||
- 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/main/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.
|
||||||
|
- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged.
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
|
|
2
.github/mergify.yml
vendored
|
@ -7,7 +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=MinGW
|
||||||
- status-success=Cygwin Test Successful
|
- status-success=Cygwin Test Successful
|
||||||
- status-success=continuous-integration/appveyor/pr
|
- status-success=continuous-integration/appveyor/pr
|
||||||
actions:
|
actions:
|
||||||
|
|
4
.github/workflows/cifuzz.yml
vendored
|
@ -3,10 +3,12 @@ name: CIFuzz
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
|
- ".github/workflows/cifuzz.yml"
|
||||||
- "**.c"
|
- "**.c"
|
||||||
- "**.h"
|
- "**.h"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
- ".github/workflows/cifuzz.yml"
|
||||||
- "**.c"
|
- "**.c"
|
||||||
- "**.h"
|
- "**.h"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -14,7 +16,7 @@ on:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
|
55
.github/workflows/docs.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
name: Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Docs
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: ".ci/*.sh"
|
||||||
|
|
||||||
|
- name: Build system information
|
||||||
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
run: |
|
||||||
|
.ci/install.sh
|
||||||
|
env:
|
||||||
|
GHA_PYTHON_VERSION: "3.x"
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
.ci/build.sh
|
||||||
|
|
||||||
|
- name: Docs
|
||||||
|
run: |
|
||||||
|
make doccheck
|
2
.github/workflows/stale.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check issues"
|
- name: "Check issues"
|
||||||
uses: actions/stale@v7
|
uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
only-labels: "Awaiting OP Action"
|
only-labels: "Awaiting OP Action"
|
||||||
|
|
34
.github/workflows/test-cygwin.yml
vendored
|
@ -1,6 +1,15 @@
|
||||||
name: Test Cygwin
|
name: Test Cygwin
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
@ -30,7 +39,7 @@ jobs:
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Cygwin
|
- name: Install Cygwin
|
||||||
uses: cygwin/cygwin-install-action@v3
|
uses: cygwin/cygwin-install-action@v4
|
||||||
with:
|
with:
|
||||||
platform: x86_64
|
platform: x86_64
|
||||||
packages: >
|
packages: >
|
||||||
|
@ -58,7 +67,6 @@ jobs:
|
||||||
python3${{ matrix.python-minor-version }}-numpy
|
python3${{ matrix.python-minor-version }}-numpy
|
||||||
python3${{ matrix.python-minor-version }}-sip
|
python3${{ matrix.python-minor-version }}-sip
|
||||||
python3${{ matrix.python-minor-version }}-tkinter
|
python3${{ matrix.python-minor-version }}-tkinter
|
||||||
qt5-devel-tools
|
|
||||||
wget
|
wget
|
||||||
xorg-server-extra
|
xorg-server-extra
|
||||||
zlib-devel
|
zlib-devel
|
||||||
|
@ -68,13 +76,23 @@ jobs:
|
||||||
with:
|
with:
|
||||||
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
||||||
|
|
||||||
|
- name: Select Python version
|
||||||
|
run: |
|
||||||
|
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
||||||
|
|
||||||
|
- name: Get latest NumPy version
|
||||||
|
id: latest-numpy
|
||||||
|
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||||
|
run: |
|
||||||
|
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: pip cache
|
- name: pip cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
||||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
|
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
|
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: |
|
run: |
|
||||||
|
@ -84,15 +102,15 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
bash.exe .ci/install.sh
|
bash.exe .ci/install.sh
|
||||||
|
|
||||||
- name: Install a different NumPy
|
- name: Install latest NumPy
|
||||||
shell: dash.exe -l "{0}"
|
shell: dash.exe -l "{0}"
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install -U 'numpy!=1.21.*'
|
python3 -m pip install -U numpy
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||||
run: |
|
run: |
|
||||||
SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh
|
.ci/build.sh
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
|
|
20
.github/workflows/test-docker.yml
vendored
|
@ -1,6 +1,15 @@
|
||||||
name: Test Docker
|
name: Test Docker
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
@ -24,16 +33,17 @@ jobs:
|
||||||
# Then run the remainder
|
# Then run the remainder
|
||||||
alpine,
|
alpine,
|
||||||
amazon-2-amd64,
|
amazon-2-amd64,
|
||||||
|
amazon-2023-amd64,
|
||||||
arch,
|
arch,
|
||||||
centos-7-amd64,
|
centos-7-amd64,
|
||||||
centos-stream-8-amd64,
|
centos-stream-8-amd64,
|
||||||
centos-stream-9-amd64,
|
centos-stream-9-amd64,
|
||||||
debian-10-buster-x86,
|
debian-11-bullseye-amd64,
|
||||||
debian-11-bullseye-x86,
|
debian-12-bookworm-x86,
|
||||||
fedora-36-amd64,
|
debian-12-bookworm-amd64,
|
||||||
fedora-37-amd64,
|
fedora-37-amd64,
|
||||||
|
fedora-38-amd64,
|
||||||
gentoo,
|
gentoo,
|
||||||
ubuntu-18.04-bionic-amd64,
|
|
||||||
ubuntu-20.04-focal-amd64,
|
ubuntu-20.04-focal-amd64,
|
||||||
ubuntu-22.04-jammy-amd64,
|
ubuntu-22.04-jammy-amd64,
|
||||||
]
|
]
|
||||||
|
|
76
.github/workflows/test-mingw.yml
vendored
|
@ -1,6 +1,15 @@
|
||||||
name: Test MinGW
|
name: Test MinGW
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
@ -12,27 +21,16 @@ concurrency:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
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:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash.exe --login -eo pipefail "{0}"
|
shell: bash.exe --login -eo pipefail "{0}"
|
||||||
env:
|
env:
|
||||||
MSYSTEM: ${{ matrix.mingw }}
|
MSYSTEM: MINGW64
|
||||||
CHERE_INVOKING: 1
|
CHERE_INVOKING: 1
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
name: ${{ matrix.name }}
|
name: "MinGW"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
|
@ -45,33 +43,29 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
pacman -S --noconfirm \
|
pacman -S --noconfirm \
|
||||||
${{ matrix.package }}-freetype \
|
mingw-w64-x86_64-freetype \
|
||||||
${{ matrix.package }}-gcc \
|
mingw-w64-x86_64-gcc \
|
||||||
${{ matrix.package }}-ghostscript \
|
mingw-w64-x86_64-ghostscript \
|
||||||
${{ matrix.package }}-lcms2 \
|
mingw-w64-x86_64-lcms2 \
|
||||||
${{ matrix.package }}-libimagequant \
|
mingw-w64-x86_64-libimagequant \
|
||||||
${{ matrix.package }}-libjpeg-turbo \
|
mingw-w64-x86_64-libjpeg-turbo \
|
||||||
${{ matrix.package }}-libraqm \
|
mingw-w64-x86_64-libraqm \
|
||||||
${{ matrix.package }}-libtiff \
|
mingw-w64-x86_64-libtiff \
|
||||||
${{ matrix.package }}-libwebp \
|
mingw-w64-x86_64-libwebp \
|
||||||
${{ matrix.package }}-openjpeg2 \
|
mingw-w64-x86_64-openjpeg2 \
|
||||||
${{ matrix.package }}-python3-cffi \
|
mingw-w64-x86_64-python3-cffi \
|
||||||
${{ matrix.package }}-python3-numpy \
|
mingw-w64-x86_64-python3-numpy \
|
||||||
${{ matrix.package }}-python3-olefile \
|
mingw-w64-x86_64-python3-olefile \
|
||||||
${{ matrix.package }}-python3-pip \
|
mingw-w64-x86_64-python3-pip \
|
||||||
${{ matrix.package }}-python3-setuptools
|
mingw-w64-x86_64-python3-setuptools \
|
||||||
|
mingw-w64-x86_64-python-pyqt6
|
||||||
if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then
|
|
||||||
pacman -S --noconfirm \
|
|
||||||
${{ matrix.package }}-python-pyqt6
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
||||||
|
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
||||||
- name: Build Pillow
|
- name: Build Pillow
|
||||||
run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" .
|
run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install .
|
||||||
|
|
||||||
- name: Test Pillow
|
- name: Test Pillow
|
||||||
run: |
|
run: |
|
||||||
|
@ -84,14 +78,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
flags: GHA_Windows
|
flags: GHA_Windows
|
||||||
name: ${{ matrix.name }}
|
name: "MSYS2 MinGW"
|
||||||
|
|
||||||
success:
|
|
||||||
permissions:
|
|
||||||
contents: none
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: MinGW Test Successful
|
|
||||||
steps:
|
|
||||||
- name: Success
|
|
||||||
run: echo MinGW Test Successful
|
|
||||||
|
|
4
.github/workflows/test-valgrind.yml
vendored
|
@ -5,10 +5,12 @@ name: Test Valgrind
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
|
- ".github/workflows/test-valgrind.yml"
|
||||||
- "**.c"
|
- "**.c"
|
||||||
- "**.h"
|
- "**.h"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
- ".github/workflows/test-valgrind.yml"
|
||||||
- "**.c"
|
- "**.c"
|
||||||
- "**.h"
|
- "**.h"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -16,7 +18,7 @@ on:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
|
54
.github/workflows/test-windows.yml
vendored
|
@ -1,6 +1,15 @@
|
||||||
name: Test Windows
|
name: Test Windows
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
@ -15,18 +24,11 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
|
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
|
||||||
architecture: ["x86", "x64"]
|
|
||||||
include:
|
|
||||||
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
|
|
||||||
- python-version: "pypy3.8"
|
|
||||||
architecture: "x64"
|
|
||||||
- python-version: "pypy3.9"
|
|
||||||
architecture: "x64"
|
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
name: Python ${{ matrix.python-version }} ${{ matrix.architecture }}
|
name: Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
|
@ -49,24 +51,23 @@ jobs:
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
architecture: ${{ matrix.architecture }}
|
|
||||||
cache: pip
|
cache: pip
|
||||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||||
|
|
||||||
- name: Print build system information
|
- name: Print build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
||||||
- name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
|
||||||
run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: install
|
id: install
|
||||||
run: |
|
run: |
|
||||||
7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\"
|
||||||
echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
|
echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
winbuild\depends\gs1000w32.exe /S
|
choco install ghostscript --version=10.0.0.20230317
|
||||||
echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH
|
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
# Install extra test images
|
# Install extra test images
|
||||||
xcopy /S /Y Tests\test-images\* Tests\images
|
xcopy /S /Y Tests\test-images\* Tests\images
|
||||||
|
@ -88,7 +89,7 @@ jobs:
|
||||||
- name: Prepare build
|
- name: Prepare build
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
& python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir
|
& python.exe winbuild\build_prepare.py -v
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Build dependencies / libjpeg-turbo
|
- name: Build dependencies / libjpeg-turbo
|
||||||
|
@ -156,9 +157,9 @@ jobs:
|
||||||
|
|
||||||
- name: Build Pillow
|
- name: Build Pillow
|
||||||
run: |
|
run: |
|
||||||
$FLAGS=""
|
$FLAGS="-C raqm=vendor -C fribidi=vendor"
|
||||||
if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" }
|
if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" }
|
||||||
& winbuild\build\build_pillow.cmd $FLAGS install
|
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
|
||||||
& $env:pythonLocation\python.exe selftest.py --installed
|
& $env:pythonLocation\python.exe selftest.py --installed
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
|
@ -197,14 +198,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
flags: GHA_Windows
|
flags: GHA_Windows
|
||||||
name: ${{ runner.os }} Python ${{ matrix.python-version }} ${{ matrix.architecture }}
|
name: ${{ runner.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Build wheel
|
- name: Build wheel
|
||||||
id: wheel
|
id: wheel
|
||||||
if: "github.event_name != 'pull_request'"
|
if: "github.event_name != 'pull_request'"
|
||||||
run: |
|
run: |
|
||||||
mkdir fribidi\${{ matrix.architecture }}
|
mkdir fribidi
|
||||||
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }}
|
copy winbuild\build\bin\fribidi* fribidi
|
||||||
setlocal EnableDelayedExpansion
|
setlocal EnableDelayedExpansion
|
||||||
for %%f in (winbuild\build\license\*) do (
|
for %%f in (winbuild\build\license\*) do (
|
||||||
set x=%%~nf
|
set x=%%~nf
|
||||||
|
@ -222,7 +223,8 @@ jobs:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
|
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
|
||||||
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
|
call winbuild\\build\\build_env.cmd
|
||||||
|
%pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable .
|
||||||
shell: cmd
|
shell: cmd
|
||||||
|
|
||||||
- name: Upload wheel
|
- name: Upload wheel
|
||||||
|
@ -230,7 +232,7 @@ jobs:
|
||||||
if: "github.event_name != 'pull_request'"
|
if: "github.event_name != 'pull_request'"
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.wheel.outputs.dist }}
|
name: ${{ steps.wheel.outputs.dist }}
|
||||||
path: dist\*.whl
|
path: "*.whl"
|
||||||
|
|
||||||
- name: Upload fribidi.dll
|
- name: Upload fribidi.dll
|
||||||
if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
|
if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
|
||||||
|
|
25
.github/workflows/test.yml
vendored
|
@ -1,6 +1,15 @@
|
||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/workflows/docs.yml"
|
||||||
|
- "docs/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
@ -20,17 +29,16 @@ jobs:
|
||||||
"ubuntu-latest",
|
"ubuntu-latest",
|
||||||
]
|
]
|
||||||
python-version: [
|
python-version: [
|
||||||
|
"pypy3.10",
|
||||||
"pypy3.9",
|
"pypy3.9",
|
||||||
"pypy3.8",
|
|
||||||
"3.12-dev",
|
"3.12-dev",
|
||||||
"3.11",
|
"3.11",
|
||||||
"3.10",
|
"3.10",
|
||||||
"3.9",
|
"3.9",
|
||||||
"3.8",
|
"3.8",
|
||||||
"3.7",
|
|
||||||
]
|
]
|
||||||
include:
|
include:
|
||||||
- python-version: "3.7"
|
- python-version: "3.9"
|
||||||
PYTHONOPTIMIZE: 1
|
PYTHONOPTIMIZE: 1
|
||||||
REVERSE: "--reverse"
|
REVERSE: "--reverse"
|
||||||
- python-version: "3.8"
|
- python-version: "3.8"
|
||||||
|
@ -76,7 +84,9 @@ jobs:
|
||||||
python3 -m pip install pytest-reverse
|
python3 -m pip install pytest-reverse
|
||||||
fi
|
fi
|
||||||
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
|
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
|
||||||
xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
|
xvfb-run -s '-screen 0 1024x768x24' sway&
|
||||||
|
export WAYLAND_DISPLAY=wayland-1
|
||||||
|
.ci/test.sh
|
||||||
else
|
else
|
||||||
.ci/test.sh
|
.ci/test.sh
|
||||||
fi
|
fi
|
||||||
|
@ -96,11 +106,6 @@ jobs:
|
||||||
name: errors
|
name: errors
|
||||||
path: Tests/errors
|
path: Tests/errors
|
||||||
|
|
||||||
- name: Docs
|
|
||||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11
|
|
||||||
run: |
|
|
||||||
make doccheck
|
|
||||||
|
|
||||||
- name: After success
|
- name: After success
|
||||||
run: |
|
run: |
|
||||||
.ci/after_success.sh
|
.ci/after_success.sh
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.1.0
|
rev: 23.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--target-version=py37]
|
args: [--target-version=py38]
|
||||||
# Only .py files, until https://github.com/psf/black/issues/402 resolved
|
|
||||||
files: \.py$
|
|
||||||
types: []
|
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 5.12.0
|
rev: 5.12.0
|
||||||
|
@ -14,25 +11,25 @@ repos:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
rev: 1.7.4
|
rev: 1.7.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: bandit
|
- id: bandit
|
||||||
args: [--severity-level=high]
|
args: [--severity-level=high]
|
||||||
files: ^src/
|
files: ^src/
|
||||||
|
|
||||||
- repo: https://github.com/asottile/yesqa
|
- repo: https://github.com/asottile/yesqa
|
||||||
rev: v1.4.0
|
rev: v1.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: yesqa
|
- id: yesqa
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.4.2
|
rev: v1.5.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 6.0.0
|
rev: 6.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
|
@ -49,6 +46,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-json
|
- id: check-json
|
||||||
|
- id: check-toml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|
||||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||||
|
@ -56,8 +54,18 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
|
rev: 0.13.0
|
||||||
|
hooks:
|
||||||
|
- id: pyproject-fmt
|
||||||
|
|
||||||
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
|
rev: v0.13
|
||||||
|
hooks:
|
||||||
|
- id: validate-pyproject
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||||
rev: 0.6.1
|
rev: 1.3.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: tox-ini-fmt
|
- id: tox-ini-fmt
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
formats: [pdf]
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
- method: pip
|
- method: pip
|
||||||
|
|
227
CHANGES.rst
|
@ -2,9 +2,234 @@
|
||||||
Changelog (Pillow)
|
Changelog (Pillow)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
9.5.0 (unreleased)
|
10.1.0 (unreleased)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Allow getpixel() to accept a list #7355
|
||||||
|
[radarhere, homm]
|
||||||
|
|
||||||
|
- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Expand JPEG buffer size when saving optimized or progressive #7345
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added session type check for Linux in ImageGrab.grabclipboard() #7332
|
||||||
|
[TheNooB2706, radarhere, hugovk]
|
||||||
|
|
||||||
|
- Read WebP duration after opening #7311
|
||||||
|
[k128, radarhere]
|
||||||
|
|
||||||
|
- Allow "loop=None" when saving GIF images #7329
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed transparency when saving P mode images to PDF #7323
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added saving LA images as PDFs #7299
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Set SMaskInData to 1 for PDFs with alpha #7316, #7317
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Changed Image mode property to be read-only by default #7307
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266
|
||||||
|
[mtreinish, radarhere]
|
||||||
|
|
||||||
|
- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fix missing symbols when libtiff depends on libjpeg #7270
|
||||||
|
[heitbaum]
|
||||||
|
|
||||||
|
10.0.0 (2023-07-01)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Fixed deallocating mask images #7246
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added ImageFont.MAX_STRING_LENGTH #7244
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Fix Windows build with pyproject.toml #7230
|
||||||
|
[hugovk, nulano, radarhere]
|
||||||
|
|
||||||
|
- Do not close provided file handles with libtiff #7199
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Convert to HSV if mode is HSV in getcolor() #7226
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added alpha_only argument to getbbox() #7123
|
||||||
|
[radarhere. hugovk]
|
||||||
|
|
||||||
|
- Prioritise speed in _repr_png_ #7242
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not use CFFI access by default on PyPy #7236
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Limit size even if one dimension is zero in decompression bomb check #7235
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Use --config-settings instead of deprecated --global-option #7171
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Better C integer definitions #6645
|
||||||
|
[Yay295, hugovk]
|
||||||
|
|
||||||
|
- Fixed finding dependencies on Cygwin #7175
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219
|
||||||
|
[abey79, radarhere]
|
||||||
|
|
||||||
|
- Added in_place argument to ImageOps.exif_transpose() #7092
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed calling putpalette() on L and LA images before load() #7187
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed saving TIFF multiframe images with LONG8 tag types #7078
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed combining single duration across duplicate APNG frames #7146
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Remove temporary file when error is raised #7148
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not use temporary file when grabbing clipboard on Linux #7200
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- If the clipboard fails to open on Windows, wait and try again #7141
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed saving multiple 1 mode frames to GIF #7181
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Replaced absolute PIL import with relative import #7173
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improved wl-paste mimetype handling in ImageGrab #7094
|
||||||
|
[rrcgat, radarhere]
|
||||||
|
|
||||||
|
- Added _repr_jpeg_() for IPython display_jpeg #7135
|
||||||
|
[n3011, radarhere, nulano]
|
||||||
|
|
||||||
|
- Use "/sbin/ldconfig" if ldconfig is not found #7068
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Prefer screenshots using XCB over gnome-screenshot #7143
|
||||||
|
[nulano, radarhere]
|
||||||
|
|
||||||
|
- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support reading signed 8-bit TIFF images #7111
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added width argument to ImageDraw regular_polygon #7132
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support I mode for ImageFilter.BuiltinFilter #7108
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added unpacker from I;16B to I;16 #7125
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support float font sizes #7107
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Use later value for duplicate xref entries in PdfParser #7102
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Load before getting size in __getstate__ #7105
|
||||||
|
[bigcat88, radarhere]
|
||||||
|
|
||||||
|
- Fixed type handling for include and lib directories #7069
|
||||||
|
[adisbladis, radarhere]
|
||||||
|
|
||||||
|
- Remove deprecations for Pillow 10.0.0 #7059, #7080
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
- Drop support for soon-EOL Python 3.7 #7058
|
||||||
|
[hugovk, radarhere]
|
||||||
|
|
||||||
|
9.5.0 (2023-04-01)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Added ImageSourceData to TAGS_V2 #7053
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Clear PPM half token after use #7052
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Removed absolute path to ldconfig #7044
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support custom comments and PLT markers when saving JPEG2000 images #6903
|
||||||
|
[joshware, radarhere, hugovk]
|
||||||
|
|
||||||
|
- Load before getting size in __array_interface__ #7034
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Consider transparency when applying APNG blend mask #7018
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Round duration when saving animated WebP images #6996
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added reading of JPEG2000 comments #6909
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Decrement reference count #7003
|
||||||
|
[radarhere, nulano]
|
||||||
|
|
||||||
|
- Allow libtiff_support_custom_tags to be missing #7020
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improved I;16N support #6834
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added QOI reading #6852
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Added saving RGBA images as PDFs #6925
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not raise an error if os.environ does not contain PATH #6935
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Close OleFileIO instance when closing or exiting FPX or MIC #7005
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added __int__ to IFDRational for Python >= 3.11 #6998
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added memoryview support to Dib.frombytes() #6988
|
||||||
|
[radarhere, nulano]
|
||||||
|
|
||||||
|
- Close file pointer copy in the libtiff encoder if still open #6986
|
||||||
|
[fcarron, radarhere]
|
||||||
|
|
||||||
|
- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added "corners" argument to ImageDraw rounded_rectangle() #6954
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Added memoryview support to frombytes() #6974
|
- Added memoryview support to frombytes() #6974
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ graft src
|
||||||
graft depends
|
graft depends
|
||||||
graft winbuild
|
graft winbuild
|
||||||
graft docs
|
graft docs
|
||||||
|
graft _custom_build
|
||||||
|
|
||||||
# build/src control detritus
|
# build/src control detritus
|
||||||
exclude .appveyor.yml
|
exclude .appveyor.yml
|
||||||
|
|
23
Makefile
|
@ -16,10 +16,16 @@ coverage:
|
||||||
python3 -m coverage report
|
python3 -m coverage report
|
||||||
|
|
||||||
.PHONY: doc
|
.PHONY: doc
|
||||||
doc:
|
.PHONY: html
|
||||||
|
doc html:
|
||||||
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
|
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
|
||||||
$(MAKE) -C docs html
|
$(MAKE) -C docs html
|
||||||
|
|
||||||
|
.PHONY: htmlview
|
||||||
|
htmlview:
|
||||||
|
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
|
||||||
|
$(MAKE) -C docs htmlview
|
||||||
|
|
||||||
.PHONY: doccheck
|
.PHONY: doccheck
|
||||||
doccheck:
|
doccheck:
|
||||||
$(MAKE) doc
|
$(MAKE) doc
|
||||||
|
@ -38,8 +44,8 @@ help:
|
||||||
@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 make HTML docs"
|
||||||
@echo " inplace make inplace extension"
|
@echo " htmlview open the index page built by the html target in your browser"
|
||||||
@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 " lint run the lint checks"
|
@echo " lint run the lint checks"
|
||||||
|
@ -47,10 +53,6 @@ help:
|
||||||
@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"
|
||||||
|
|
||||||
.PHONY: inplace
|
|
||||||
inplace: clean
|
|
||||||
python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" .
|
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install:
|
install:
|
||||||
python3 -m pip -v install .
|
python3 -m pip -v install .
|
||||||
|
@ -58,7 +60,7 @@ install:
|
||||||
|
|
||||||
.PHONY: install-coverage
|
.PHONY: install-coverage
|
||||||
install-coverage:
|
install-coverage:
|
||||||
CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" .
|
CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install .
|
||||||
python3 selftest.py
|
python3 selftest.py
|
||||||
|
|
||||||
.PHONY: debug
|
.PHONY: debug
|
||||||
|
@ -67,10 +69,11 @@ 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 -m pip -v install --global-option="build_ext" . > /dev/null
|
CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null
|
||||||
|
|
||||||
.PHONY: release-test
|
.PHONY: release-test
|
||||||
release-test:
|
release-test:
|
||||||
|
python3 Tests/check_release_notes.py
|
||||||
python3 -m pip install -e .[tests]
|
python3 -m pip install -e .[tests]
|
||||||
python3 selftest.py
|
python3 selftest.py
|
||||||
python3 -m pytest Tests
|
python3 -m pytest Tests
|
||||||
|
@ -116,5 +119,5 @@ lint:
|
||||||
lint-fix:
|
lint-fix:
|
||||||
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
|
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
|
||||||
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
|
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
|
||||||
python3 -m black --target-version py37 .
|
python3 -m black --target-version py38 .
|
||||||
python3 -m isort .
|
python3 -m isort .
|
||||||
|
|
46
RELEASING.md
|
@ -11,14 +11,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
* [ ] Develop and prepare release in `main` branch.
|
* [ ] Develop and prepare release in `main` branch.
|
||||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
||||||
* [ ] Check that all of the wheel builds [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://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||||
* [ ] Update `CHANGES.rst`.
|
* [ ] Update `CHANGES.rst`.
|
||||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||||
* [ ] Create branch and tag for release e.g.:
|
* [ ] Create branch and tag for release e.g.:
|
||||||
```bash
|
```bash
|
||||||
git branch 5.2.x
|
git branch 5.2.x
|
||||||
git tag 5.2.0
|
git tag 5.2.0
|
||||||
git push --all
|
|
||||||
git push --tags
|
git push --tags
|
||||||
```
|
```
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
|
@ -32,8 +31,11 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
python3 -m 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://peps.python.org/pep-0440/),
|
||||||
|
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
|
||||||
|
```bash
|
||||||
|
git push --all
|
||||||
|
```
|
||||||
## Point Release
|
## Point Release
|
||||||
|
|
||||||
Released as needed for security, installation or critical bug fixes.
|
Released as needed for security, installation or critical bug fixes.
|
||||||
|
@ -45,16 +47,12 @@ Released as needed for security, installation or critical bug fixes.
|
||||||
git checkout -t remotes/origin/5.2.x
|
git checkout -t remotes/origin/5.2.x
|
||||||
```
|
```
|
||||||
* [ ] Cherry pick individual commits from `main` 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`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* [ ] 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 release branch e.g. `5.2.x`.
|
* [ ] 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 release branch e.g. `5.2.x`.
|
||||||
* [ ] 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://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||||
* [ ] Run pre-release check via `make release-test`.
|
* [ ] Run pre-release check via `make release-test`.
|
||||||
* [ ] Create tag for release e.g.:
|
* [ ] Create tag for release e.g.:
|
||||||
```bash
|
```bash
|
||||||
git tag 5.2.1
|
git tag 5.2.1
|
||||||
git push
|
|
||||||
git push --tags
|
git push --tags
|
||||||
```
|
```
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
|
@ -67,7 +65,10 @@ Released as needed for security, installation or critical bug fixes.
|
||||||
python3 -m twine check --strict dist/*
|
python3 -m twine check --strict dist/*
|
||||||
python3 -m 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) and then:
|
||||||
|
```bash
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
## Embargoed Release
|
## Embargoed Release
|
||||||
|
|
||||||
|
@ -83,7 +84,6 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
```bash
|
```bash
|
||||||
git checkout 2.5.x
|
git checkout 2.5.x
|
||||||
git tag 2.5.3
|
git tag 2.5.3
|
||||||
git push origin 2.5.x
|
|
||||||
git push origin --tags
|
git push origin --tags
|
||||||
```
|
```
|
||||||
* [ ] Create and check source distribution:
|
* [ ] Create and check source distribution:
|
||||||
|
@ -91,15 +91,14 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
make sdist
|
make sdist
|
||||||
```
|
```
|
||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/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) and then:
|
||||||
|
```bash
|
||||||
|
git push origin 2.5.x
|
||||||
|
```
|
||||||
|
|
||||||
## Binary Distributions
|
## Binary Distributions
|
||||||
|
|
||||||
### Windows
|
### macOS and Linux
|
||||||
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
|
||||||
and copy into `dist/`
|
|
||||||
|
|
||||||
### Mac and Linux
|
|
||||||
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/python-pillow/pillow-wheels
|
git clone https://github.com/python-pillow/pillow-wheels
|
||||||
|
@ -107,7 +106,18 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
./update-pillow-tag.sh [[release tag]]
|
./update-pillow-tag.sh [[release tag]]
|
||||||
```
|
```
|
||||||
* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
|
* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
|
||||||
and copy into `dist/`
|
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo:
|
||||||
|
```bash
|
||||||
|
gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
||||||
|
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
|
||||||
|
```bash
|
||||||
|
gh run download --dir dist
|
||||||
|
# select dist-x.y.z
|
||||||
|
```
|
||||||
|
|
||||||
## Publicize Release
|
## Publicize Release
|
||||||
|
|
||||||
|
|
|
@ -27,25 +27,19 @@ def timer(func, label, *args):
|
||||||
for x in range(iterations):
|
for x in range(iterations):
|
||||||
func(*args)
|
func(*args)
|
||||||
if time.time() - starttime > 10:
|
if time.time() - starttime > 10:
|
||||||
print(
|
|
||||||
"{}: breaking at {} iterations, {:.6f} per iteration".format(
|
|
||||||
label, x + 1, (time.time() - starttime) / (x + 1.0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
if x == iterations - 1:
|
endtime = time.time()
|
||||||
endtime = time.time()
|
print(
|
||||||
print(
|
"{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format(
|
||||||
"{}: {:.4f} s {:.6f} per iteration".format(
|
label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0)
|
||||||
label, endtime - starttime, (endtime - starttime) / (x + 1.0)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_direct():
|
def test_direct():
|
||||||
im = hopper()
|
im = hopper()
|
||||||
im.load()
|
im.load()
|
||||||
# im = Image.new( "RGB", (2000, 2000), (1, 3, 2))
|
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
|
||||||
caccess = im.im.pixel_access(False)
|
caccess = im.im.pixel_access(False)
|
||||||
access = PyAccess.new(im, False)
|
access = PyAccess.new(im, False)
|
||||||
|
|
||||||
|
|
|
@ -75,43 +75,42 @@ post-patch:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def test_qtables_leak():
|
standard_l_qtable = (
|
||||||
|
# fmt: off
|
||||||
|
16, 11, 10, 16, 24, 40, 51, 61,
|
||||||
|
12, 12, 14, 19, 26, 58, 60, 55,
|
||||||
|
14, 13, 16, 24, 40, 57, 69, 56,
|
||||||
|
14, 17, 22, 29, 51, 87, 80, 62,
|
||||||
|
18, 22, 37, 56, 68, 109, 103, 77,
|
||||||
|
24, 35, 55, 64, 81, 104, 113, 92,
|
||||||
|
49, 64, 78, 87, 103, 121, 120, 101,
|
||||||
|
72, 92, 95, 98, 112, 100, 103, 99,
|
||||||
|
# fmt: on
|
||||||
|
)
|
||||||
|
|
||||||
|
standard_chrominance_qtable = (
|
||||||
|
# fmt: off
|
||||||
|
17, 18, 24, 47, 99, 99, 99, 99,
|
||||||
|
18, 21, 26, 66, 99, 99, 99, 99,
|
||||||
|
24, 26, 56, 99, 99, 99, 99, 99,
|
||||||
|
47, 66, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
99, 99, 99, 99, 99, 99, 99, 99,
|
||||||
|
# fmt: on
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"qtables",
|
||||||
|
(
|
||||||
|
(standard_l_qtable, standard_chrominance_qtable),
|
||||||
|
[standard_l_qtable, standard_chrominance_qtable],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_qtables_leak(qtables):
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
|
|
||||||
standard_l_qtable = [
|
|
||||||
int(s)
|
|
||||||
for s in """
|
|
||||||
16 11 10 16 24 40 51 61
|
|
||||||
12 12 14 19 26 58 60 55
|
|
||||||
14 13 16 24 40 57 69 56
|
|
||||||
14 17 22 29 51 87 80 62
|
|
||||||
18 22 37 56 68 109 103 77
|
|
||||||
24 35 55 64 81 104 113 92
|
|
||||||
49 64 78 87 103 121 120 101
|
|
||||||
72 92 95 98 112 100 103 99
|
|
||||||
""".split(
|
|
||||||
None
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
standard_chrominance_qtable = [
|
|
||||||
int(s)
|
|
||||||
for s in """
|
|
||||||
17 18 24 47 99 99 99 99
|
|
||||||
18 21 26 66 99 99 99 99
|
|
||||||
24 26 56 99 99 99 99 99
|
|
||||||
47 66 99 99 99 99 99 99
|
|
||||||
99 99 99 99 99 99 99 99
|
|
||||||
99 99 99 99 99 99 99 99
|
|
||||||
99 99 99 99 99 99 99 99
|
|
||||||
99 99 99 99 99 99 99 99
|
|
||||||
""".split(
|
|
||||||
None
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
qtables = [standard_l_qtable, standard_chrominance_qtable]
|
|
||||||
|
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
test_output = BytesIO()
|
test_output = BytesIO()
|
||||||
im.save(test_output, "JPEG", qtables=qtables)
|
im.save(test_output, "JPEG", qtables=qtables)
|
||||||
|
|
6
Tests/check_release_notes.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
for rst in Path("docs/releasenotes").glob("[1-9]*.rst"):
|
||||||
|
if "TODO" in open(rst).read():
|
||||||
|
sys.exit(f"Error: remove TODO from {rst}")
|
BIN
Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf
Normal file
|
@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
HAS_UPLOADER = False
|
HAS_UPLOADER = False
|
||||||
|
|
||||||
if os.environ.get("SHOW_ERRORS", None):
|
if os.environ.get("SHOW_ERRORS"):
|
||||||
# local img.show for errors.
|
# local img.show for errors.
|
||||||
HAS_UPLOADER = True
|
HAS_UPLOADER = True
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ def netpbm_available():
|
||||||
|
|
||||||
def magick_command():
|
def magick_command():
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
magickhome = os.environ.get("MAGICK_HOME", "")
|
magickhome = os.environ.get("MAGICK_HOME")
|
||||||
if magickhome:
|
if magickhome:
|
||||||
imagemagick = [os.path.join(magickhome, "convert.exe")]
|
imagemagick = [os.path.join(magickhome, "convert.exe")]
|
||||||
graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"]
|
graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"]
|
||||||
|
|
BIN
Tests/images/8bit.s.tif
Normal file
BIN
Tests/images/blend_transparency.png
Normal file
After Width: | Height: | Size: 211 B |
BIN
Tests/images/comment.jp2
Normal file
BIN
Tests/images/duplicate_xref_entry.pdf
Normal file
BIN
Tests/images/hopper.qoi
Normal file
BIN
Tests/images/hopper_emboss_I.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
Tests/images/hopper_emboss_more_I.png
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png
Normal file
After Width: | Height: | Size: 544 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nnny.png
Normal file
After Width: | Height: | Size: 685 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png
Normal file
After Width: | Height: | Size: 649 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png
Normal file
After Width: | Height: | Size: 755 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nynn.png
Normal file
After Width: | Height: | Size: 643 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nyny.png
Normal file
After Width: | Height: | Size: 775 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png
Normal file
After Width: | Height: | Size: 741 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png
Normal file
After Width: | Height: | Size: 656 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_ynny.png
Normal file
After Width: | Height: | Size: 785 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png
Normal file
After Width: | Height: | Size: 752 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png
Normal file
After Width: | Height: | Size: 856 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_yynn.png
Normal file
After Width: | Height: | Size: 737 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_yyny.png
Normal file
After Width: | Height: | Size: 870 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png
Normal file
After Width: | Height: | Size: 835 B |
BIN
Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png
Normal file
After Width: | Height: | Size: 934 B |
BIN
Tests/images/imagedraw_rounded_rectangle_x_odd.png
Normal file
After Width: | Height: | Size: 565 B |
BIN
Tests/images/imagedraw_rounded_rectangle_y_odd.png
Normal file
After Width: | Height: | Size: 527 B |
BIN
Tests/images/imagedraw_triangle_width.png
Normal file
After Width: | Height: | Size: 499 B |
BIN
Tests/images/orientation_rectangle.jpg
Normal file
After Width: | Height: | Size: 669 B |
BIN
Tests/images/pil123rgba.qoi
Normal file
BIN
Tests/images/zero_width.gif
Normal file
After Width: | Height: | Size: 44 B |
|
@ -20,7 +20,7 @@ python3 setup.py build --build-base=/tmp/build install
|
||||||
# Build fuzzers in $OUT.
|
# Build fuzzers in $OUT.
|
||||||
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
|
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
|
||||||
compile_python_fuzzer $fuzzer \
|
compile_python_fuzzer $fuzzer \
|
||||||
--add-binary /usr/local/lib/libjpeg.so.62.3.0:. \
|
--add-binary /usr/local/lib/libjpeg.so.62.4.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:. \
|
||||||
|
|
|
@ -6,6 +6,7 @@ import packaging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, features
|
from PIL import Image, features
|
||||||
|
from Tests.helper import skip_unless_feature
|
||||||
|
|
||||||
if sys.platform.startswith("win32"):
|
if sys.platform.startswith("win32"):
|
||||||
pytest.skip("Fuzzer is linux only", allow_module_level=True)
|
pytest.skip("Fuzzer is linux only", allow_module_level=True)
|
||||||
|
@ -48,6 +49,7 @@ def test_fuzz_images(path):
|
||||||
fuzzers.disable_decompressionbomb_error()
|
fuzzers.disable_decompressionbomb_error()
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("freetype2")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
|
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,7 +22,7 @@ def test_imageops_box_blur():
|
||||||
|
|
||||||
|
|
||||||
def box_blur(image, radius=1, n=1):
|
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, radius), n))
|
||||||
|
|
||||||
|
|
||||||
def assert_image(im, data, delta=0):
|
def assert_image(im, data, delta=0):
|
||||||
|
|
|
@ -177,13 +177,14 @@ class TestEnvVars:
|
||||||
Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"})
|
Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"})
|
||||||
assert Image.core.get_block_size() == 2 * 1024 * 1024
|
assert Image.core.get_block_size() == 2 * 1024 * 1024
|
||||||
|
|
||||||
def test_warnings(self):
|
@pytest.mark.parametrize(
|
||||||
pytest.warns(
|
"var",
|
||||||
UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"}
|
(
|
||||||
)
|
{"PILLOW_ALIGNMENT": "15"},
|
||||||
pytest.warns(
|
{"PILLOW_BLOCK_SIZE": "1024"},
|
||||||
UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"}
|
{"PILLOW_BLOCKS_MAX": "wat"},
|
||||||
)
|
),
|
||||||
pytest.warns(
|
)
|
||||||
UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"}
|
def test_warnings(self, var):
|
||||||
)
|
with pytest.warns(UserWarning):
|
||||||
|
Image._apply_env_variables(var)
|
||||||
|
|
|
@ -36,12 +36,10 @@ class TestDecompressionBomb:
|
||||||
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
|
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
|
||||||
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
|
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
|
||||||
|
|
||||||
def open():
|
with pytest.warns(Image.DecompressionBombWarning):
|
||||||
with Image.open(TEST_FILE):
|
with Image.open(TEST_FILE):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
pytest.warns(Image.DecompressionBombWarning, open)
|
|
||||||
|
|
||||||
def test_exception(self):
|
def test_exception(self):
|
||||||
# Set limit to trigger exception on the test file
|
# Set limit to trigger exception on the test file
|
||||||
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
|
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
|
||||||
|
@ -66,6 +64,15 @@ class TestDecompressionBomb:
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
|
||||||
|
def test_exception_gif_zero_width(self):
|
||||||
|
# Set limit to trigger exception on the test file
|
||||||
|
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
|
||||||
|
assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
|
||||||
|
|
||||||
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
|
with Image.open("Tests/images/zero_width.gif"):
|
||||||
|
pass
|
||||||
|
|
||||||
def test_exception_bmp(self):
|
def test_exception_bmp(self):
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
|
with Image.open("Tests/images/bmp/b/reallybig.bmp"):
|
||||||
|
@ -87,7 +94,8 @@ class TestDecompressionCrop:
|
||||||
# same decompression bomb warnings on them.
|
# same decompression bomb warnings on them.
|
||||||
with hopper() as src:
|
with hopper() as src:
|
||||||
box = (0, 0, src.width * 2, src.height * 2)
|
box = (0, 0, src.width * 2, src.height * 2)
|
||||||
pytest.warns(Image.DecompressionBombWarning, src.crop, box)
|
with 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))
|
||||||
|
@ -95,7 +103,8 @@ class TestDecompressionCrop:
|
||||||
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):
|
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):
|
||||||
assert im.crop(value).size == (9, 9)
|
assert im.crop(value).size == (9, 9)
|
||||||
|
|
||||||
pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99))
|
with pytest.warns(Image.DecompressionBombWarning):
|
||||||
|
im.crop((-160, -160, 99, 99))
|
||||||
|
|
||||||
with pytest.raises(Image.DecompressionBombError):
|
with pytest.raises(Image.DecompressionBombError):
|
||||||
im.crop((-99909, -99990, 99999, 99999))
|
im.crop((-99909, -99990, 99999, 99999))
|
||||||
|
|
|
@ -6,11 +6,6 @@ from PIL import _deprecate
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"version, expected",
|
"version, expected",
|
||||||
[
|
[
|
||||||
(
|
|
||||||
10,
|
|
||||||
"Old thing is deprecated and will be removed in Pillow 10 "
|
|
||||||
r"\(2023-07-01\)\. Use new thing instead\.",
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
11,
|
11,
|
||||||
"Old thing is deprecated and will be removed in Pillow 11 "
|
"Old thing is deprecated and will be removed in Pillow 11 "
|
||||||
|
@ -29,7 +24,7 @@ def test_version(version, expected):
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_version():
|
def test_unknown_version():
|
||||||
expected = r"Unknown removal version, update PIL\._deprecate\?"
|
expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?"
|
||||||
with pytest.raises(ValueError, match=expected):
|
with pytest.raises(ValueError, match=expected):
|
||||||
_deprecate.deprecate("Old thing", 12345, "new thing")
|
_deprecate.deprecate("Old thing", 12345, "new thing")
|
||||||
|
|
||||||
|
@ -57,18 +52,18 @@ def test_old_version(deprecated, plural, expected):
|
||||||
|
|
||||||
def test_plural():
|
def test_plural():
|
||||||
expected = (
|
expected = (
|
||||||
r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
|
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||||
r"Use new thing instead\."
|
r"Use new thing instead\."
|
||||||
)
|
)
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old things", 10, "new thing", plural=True)
|
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
|
||||||
|
|
||||||
|
|
||||||
def test_replacement_and_action():
|
def test_replacement_and_action():
|
||||||
expected = "Use only one of 'replacement' and 'action'"
|
expected = "Use only one of 'replacement' and 'action'"
|
||||||
with pytest.raises(ValueError, match=expected):
|
with pytest.raises(ValueError, match=expected):
|
||||||
_deprecate.deprecate(
|
_deprecate.deprecate(
|
||||||
"Old thing", 10, replacement="new thing", action="Upgrade to new thing"
|
"Old thing", 11, replacement="new thing", action="Upgrade to new thing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,16 +76,16 @@ def test_replacement_and_action():
|
||||||
)
|
)
|
||||||
def test_action(action):
|
def test_action(action):
|
||||||
expected = (
|
expected = (
|
||||||
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
|
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||||
r"Upgrade to new thing\."
|
r"Upgrade to new thing\."
|
||||||
)
|
)
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old thing", 10, action=action)
|
_deprecate.deprecate("Old thing", 11, action=action)
|
||||||
|
|
||||||
|
|
||||||
def test_no_replacement_or_action():
|
def test_no_replacement_or_action():
|
||||||
expected = (
|
expected = (
|
||||||
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)"
|
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
|
||||||
)
|
)
|
||||||
with pytest.warns(DeprecationWarning, match=expected):
|
with pytest.warns(DeprecationWarning, match=expected):
|
||||||
_deprecate.deprecate("Old thing", 10)
|
_deprecate.deprecate("Old thing", 11)
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import warnings
|
|
||||||
|
|
||||||
with warnings.catch_warnings(record=True) as w:
|
|
||||||
# Arrange: cause all warnings to always be triggered
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
|
|
||||||
# Act: trigger a warning with Qt5
|
|
||||||
from PIL import ImageQt
|
|
||||||
|
|
||||||
|
|
||||||
def test_deprecated():
|
|
||||||
# Assert
|
|
||||||
if ImageQt.qt_version in ("5", "side2"):
|
|
||||||
assert len(w) == 1
|
|
||||||
assert issubclass(w[0].category, DeprecationWarning)
|
|
||||||
assert "deprecated" in str(w[0].message)
|
|
||||||
else:
|
|
||||||
assert len(w) == 0
|
|
|
@ -163,6 +163,12 @@ def test_apng_blend():
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apng_blend_transparency():
|
||||||
|
with Image.open("Tests/images/blend_transparency.png") as im:
|
||||||
|
im.seek(1)
|
||||||
|
assert im.getpixel((0, 0)) == (255, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_chunk_order():
|
def test_apng_chunk_order():
|
||||||
with Image.open("Tests/images/apng/fctl_actl.png") as im:
|
with Image.open("Tests/images/apng/fctl_actl.png") as im:
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
@ -263,13 +269,11 @@ def test_apng_chunk_errors():
|
||||||
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
def open():
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
||||||
im.load()
|
im.load()
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
pytest.warns(UserWarning, open)
|
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
|
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
|
@ -287,21 +291,17 @@ def test_apng_chunk_errors():
|
||||||
|
|
||||||
|
|
||||||
def test_apng_syntax_errors():
|
def test_apng_syntax_errors():
|
||||||
def open_frames_zero():
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(UserWarning, open_frames_zero)
|
with pytest.warns(UserWarning):
|
||||||
|
|
||||||
def open_frames_zero_default():
|
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(UserWarning, open_frames_zero_default)
|
|
||||||
|
|
||||||
# we can handle this case gracefully
|
# we can handle this case gracefully
|
||||||
exception = None
|
exception = None
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
||||||
|
@ -316,13 +316,11 @@ def test_apng_syntax_errors():
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
def open():
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(UserWarning, open)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file",
|
"test_file",
|
||||||
|
@ -376,6 +374,20 @@ def test_apng_save(tmp_path):
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def test_apng_save_alpha(tmp_path):
|
||||||
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
|
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
|
||||||
|
im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
|
||||||
|
im.save(test_file, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255)
|
||||||
|
|
||||||
|
reloaded.seek(1)
|
||||||
|
assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)
|
||||||
|
|
||||||
|
|
||||||
def test_apng_save_split_fdat(tmp_path):
|
def test_apng_save_split_fdat(tmp_path):
|
||||||
# test to make sure we do not generate sequence errors when writing
|
# test to make sure we do not generate sequence errors when writing
|
||||||
# frames with image data spanning multiple fdAT chunks (in this case
|
# frames with image data spanning multiple fdAT chunks (in this case
|
||||||
|
@ -449,6 +461,17 @@ def test_apng_save_duration_loop(tmp_path):
|
||||||
assert im.info.get("duration") == 750
|
assert im.info.get("duration") == 750
|
||||||
|
|
||||||
|
|
||||||
|
def test_apng_save_duplicate_duration(tmp_path):
|
||||||
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
frame = Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
# Test a single duration is correctly combined across duplicate frames
|
||||||
|
frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
assert im.n_frames == 1
|
||||||
|
assert im.info.get("duration") == 1500
|
||||||
|
|
||||||
|
|
||||||
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")
|
||||||
size = (128, 64)
|
size = (128, 64)
|
||||||
|
@ -657,13 +680,3 @@ def test_different_modes_in_later_frames(mode, tmp_path):
|
||||||
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
|
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
assert reloaded.mode == mode
|
assert reloaded.mode == mode
|
||||||
|
|
||||||
|
|
||||||
def test_constants_deprecation():
|
|
||||||
for enum, prefix in {
|
|
||||||
PngImagePlugin.Disposal: "APNG_DISPOSE_",
|
|
||||||
PngImagePlugin.Blend: "APNG_BLEND_",
|
|
||||||
}.items():
|
|
||||||
for name in enum.__members__:
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert getattr(PngImagePlugin, prefix + name) == enum[name]
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import BlpImagePlugin, Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -72,14 +72,3 @@ 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]
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ def test_handler(tmp_path):
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename):
|
def save(self, im, fp, filename):
|
||||||
|
|
|
@ -28,7 +28,8 @@ def test_unclosed_file():
|
||||||
im = Image.open(TEST_FILE)
|
im = Image.open(TEST_FILE)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file():
|
||||||
|
|
|
@ -28,34 +28,65 @@ FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png"
|
||||||
# EPS test files with binary preview
|
# EPS test files with binary preview
|
||||||
FILE3 = "Tests/images/binary_preview_map.eps"
|
FILE3 = "Tests/images/binary_preview_map.eps"
|
||||||
|
|
||||||
|
# Three unsigned 32bit little-endian values:
|
||||||
|
# 0xC6D3D0C5 magic number
|
||||||
|
# byte position of start of postscript section (12)
|
||||||
|
# byte length of postscript section (0)
|
||||||
|
# this byte length isn't valid, but we don't read it
|
||||||
|
simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
# taken from page 8 of the specification
|
||||||
|
# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf
|
||||||
|
simple_eps_file = (
|
||||||
|
b"%!PS-Adobe-3.0 EPSF-3.0",
|
||||||
|
b"%%BoundingBox: 5 5 105 105",
|
||||||
|
b"10 setlinewidth",
|
||||||
|
b"10 10 moveto",
|
||||||
|
b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath",
|
||||||
|
b"stroke",
|
||||||
|
)
|
||||||
|
simple_eps_file_with_comments = (
|
||||||
|
simple_eps_file[:1]
|
||||||
|
+ (
|
||||||
|
b"%%Comment1: Some Value",
|
||||||
|
b"%%SecondComment: Another Value",
|
||||||
|
)
|
||||||
|
+ simple_eps_file[1:]
|
||||||
|
)
|
||||||
|
simple_eps_file_without_version = simple_eps_file[1:]
|
||||||
|
simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:]
|
||||||
|
simple_eps_file_with_invalid_boundingbox = (
|
||||||
|
simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:]
|
||||||
|
)
|
||||||
|
simple_eps_file_with_invalid_boundingbox_valid_imagedata = (
|
||||||
|
simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",)
|
||||||
|
)
|
||||||
|
simple_eps_file_with_long_ascii_comment = (
|
||||||
|
simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:]
|
||||||
|
)
|
||||||
|
simple_eps_file_with_long_binary_data = (
|
||||||
|
simple_eps_file[:2]
|
||||||
|
+ (
|
||||||
|
b"%%BeginBinary: 300",
|
||||||
|
b"\0" * 300,
|
||||||
|
b"%%EndBinary",
|
||||||
|
)
|
||||||
|
+ simple_eps_file[2:]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_sanity():
|
@pytest.mark.parametrize(
|
||||||
# Regular scale
|
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
|
||||||
with Image.open(FILE1) as image1:
|
)
|
||||||
image1.load()
|
@pytest.mark.parametrize("scale", (1, 2))
|
||||||
assert image1.mode == "RGB"
|
def test_sanity(filename, size, scale):
|
||||||
assert image1.size == (460, 352)
|
expected_size = tuple(s * scale for s in size)
|
||||||
assert image1.format == "EPS"
|
with Image.open(filename) as image:
|
||||||
|
image.load(scale=scale)
|
||||||
with Image.open(FILE2) as image2:
|
assert image.mode == "RGB"
|
||||||
image2.load()
|
assert image.size == expected_size
|
||||||
assert image2.mode == "RGB"
|
assert image.format == "EPS"
|
||||||
assert image2.size == (360, 252)
|
|
||||||
assert image2.format == "EPS"
|
|
||||||
|
|
||||||
# Double scale
|
|
||||||
with Image.open(FILE1) as image1_scale2:
|
|
||||||
image1_scale2.load(scale=2)
|
|
||||||
assert image1_scale2.mode == "RGB"
|
|
||||||
assert image1_scale2.size == (920, 704)
|
|
||||||
assert image1_scale2.format == "EPS"
|
|
||||||
|
|
||||||
with Image.open(FILE2) as image2_scale2:
|
|
||||||
image2_scale2.load(scale=2)
|
|
||||||
assert image2_scale2.mode == "RGB"
|
|
||||||
assert image2_scale2.size == (720, 504)
|
|
||||||
assert image2_scale2.format == "EPS"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
|
@ -69,11 +100,72 @@ def test_load():
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
EpsImagePlugin.EpsImageFile(invalid_file)
|
EpsImagePlugin.EpsImageFile(invalid_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_binary_header_only():
|
||||||
|
data = io.BytesIO(simple_binary_header)
|
||||||
|
with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'):
|
||||||
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
def test_missing_version_comment(prefix):
|
||||||
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
def test_missing_boundingbox_comment(prefix):
|
||||||
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
|
||||||
|
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
|
||||||
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
def test_invalid_boundingbox_comment(prefix):
|
||||||
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
|
||||||
|
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
|
||||||
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix):
|
||||||
|
data = io.BytesIO(
|
||||||
|
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
|
||||||
|
)
|
||||||
|
with Image.open(data) as img:
|
||||||
|
assert img.mode == "RGB"
|
||||||
|
assert img.size == (100, 100)
|
||||||
|
assert img.format == "EPS"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
def test_ascii_comment_too_long(prefix):
|
||||||
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
|
||||||
|
with pytest.raises(SyntaxError, match="not an EPS file"):
|
||||||
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
def test_long_binary_data(prefix):
|
||||||
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
||||||
|
EpsImagePlugin.EpsImageFile(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
def test_load_long_binary_data(prefix):
|
||||||
|
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
|
||||||
|
with Image.open(data) as img:
|
||||||
|
img.load()
|
||||||
|
assert img.mode == "RGB"
|
||||||
|
assert img.size == (100, 100)
|
||||||
|
assert img.format == "EPS"
|
||||||
|
|
||||||
|
|
||||||
@mark_if_feature_version(
|
@mark_if_feature_version(
|
||||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||||
)
|
)
|
||||||
|
@ -100,7 +192,7 @@ def test_showpage():
|
||||||
with Image.open("Tests/images/reqd_showpage.png") as target:
|
with Image.open("Tests/images/reqd_showpage.png") as target:
|
||||||
# should not crash/hang
|
# should not crash/hang
|
||||||
plot_image.load()
|
plot_image.load()
|
||||||
# fonts could be slightly different
|
# fonts could be slightly different
|
||||||
assert_image_similar(plot_image, target, 6)
|
assert_image_similar(plot_image, target, 6)
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,7 +203,7 @@ def test_transparency():
|
||||||
assert plot_image.mode == "RGBA"
|
assert plot_image.mode == "RGBA"
|
||||||
|
|
||||||
with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
|
with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
|
||||||
# fonts could be slightly different
|
# fonts could be slightly different
|
||||||
assert_image_similar(plot_image, target, 6)
|
assert_image_similar(plot_image, target, 6)
|
||||||
|
|
||||||
|
|
||||||
|
@ -206,7 +298,6 @@ def test_resize(filename):
|
||||||
@pytest.mark.parametrize("filename", (FILE1, FILE2))
|
@pytest.mark.parametrize("filename", (FILE1, FILE2))
|
||||||
def test_thumbnail(filename):
|
def test_thumbnail(filename):
|
||||||
# Issue #619
|
# Issue #619
|
||||||
# Arrange
|
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
new_size = (100, 100)
|
new_size = (100, 100)
|
||||||
im.thumbnail(new_size)
|
im.thumbnail(new_size)
|
||||||
|
@ -220,7 +311,7 @@ def test_read_binary_preview():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_readline(tmp_path):
|
def test_readline_psfile(tmp_path):
|
||||||
# check all the freaking line endings possible from the spec
|
# check all the freaking line endings possible from the spec
|
||||||
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
|
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
|
||||||
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
||||||
|
@ -237,7 +328,8 @@ def test_readline(tmp_path):
|
||||||
|
|
||||||
def _test_readline_io_psfile(test_string, ending):
|
def _test_readline_io_psfile(test_string, ending):
|
||||||
f = io.BytesIO(test_string.encode("latin-1"))
|
f = io.BytesIO(test_string.encode("latin-1"))
|
||||||
t = EpsImagePlugin.PSFile(f)
|
with pytest.warns(DeprecationWarning):
|
||||||
|
t = EpsImagePlugin.PSFile(f)
|
||||||
_test_readline(t, ending)
|
_test_readline(t, ending)
|
||||||
|
|
||||||
def _test_readline_file_psfile(test_string, ending):
|
def _test_readline_file_psfile(test_string, ending):
|
||||||
|
@ -246,7 +338,8 @@ def test_readline(tmp_path):
|
||||||
w.write(test_string.encode("latin-1"))
|
w.write(test_string.encode("latin-1"))
|
||||||
|
|
||||||
with open(f, "rb") as r:
|
with open(f, "rb") as r:
|
||||||
t = EpsImagePlugin.PSFile(r)
|
with pytest.warns(DeprecationWarning):
|
||||||
|
t = EpsImagePlugin.PSFile(r)
|
||||||
_test_readline(t, ending)
|
_test_readline(t, ending)
|
||||||
|
|
||||||
for ending in line_endings:
|
for ending in line_endings:
|
||||||
|
@ -255,6 +348,25 @@ def test_readline(tmp_path):
|
||||||
_test_readline_file_psfile(s, ending)
|
_test_readline_file_psfile(s, ending)
|
||||||
|
|
||||||
|
|
||||||
|
def test_psfile_deprecation():
|
||||||
|
with pytest.warns(DeprecationWarning):
|
||||||
|
EpsImagePlugin.PSFile(None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"line_ending",
|
||||||
|
(b"\r\n", b"\n", b"\n\r", b"\r"),
|
||||||
|
)
|
||||||
|
def test_readline(prefix, line_ending):
|
||||||
|
simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
|
||||||
|
data = io.BytesIO(simple_file)
|
||||||
|
test_file = EpsImagePlugin.EpsImageFile(data)
|
||||||
|
assert test_file.info["Comment1"] == "Some Value"
|
||||||
|
assert test_file.info["SecondComment"] == "Another Value"
|
||||||
|
assert test_file.size == (100, 100)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"filename",
|
"filename",
|
||||||
(
|
(
|
||||||
|
|
|
@ -2,7 +2,7 @@ from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import FitsImagePlugin, FitsStubImagePlugin, Image
|
from PIL import FitsImagePlugin, Image
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper
|
from .helper import assert_image_equal, hopper
|
||||||
|
|
||||||
|
@ -48,38 +48,3 @@ def test_comment():
|
||||||
image_data = b"SIMPLE = T / comment string"
|
image_data = b"SIMPLE = T / comment string"
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
|
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
|
@ -36,7 +36,8 @@ def test_unclosed_file():
|
||||||
im = Image.open(static_test_file)
|
im = Image.open(static_test_file)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file():
|
||||||
|
|
|
@ -18,6 +18,16 @@ def test_sanity():
|
||||||
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
|
assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_close():
|
||||||
|
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||||
|
pass
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
im = Image.open("Tests/images/input_bw_one_band.fpx")
|
||||||
|
im.close()
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
# Test an invalid OLE file
|
# Test an invalid OLE file
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
|
@ -21,12 +21,3 @@ def test_invalid_file():
|
||||||
|
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
FtexImagePlugin.FtexImageFile(invalid_file)
|
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]
|
|
||||||
|
|
|
@ -36,7 +36,8 @@ def test_unclosed_file():
|
||||||
im = Image.open(TEST_GIF)
|
im = Image.open(TEST_GIF)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file():
|
||||||
|
@ -251,6 +252,19 @@ def test_roundtrip_save_all(tmp_path):
|
||||||
assert reread.n_frames == 5
|
assert reread.n_frames == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_roundtrip_save_all_1(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
im = Image.new("1", (1, 1))
|
||||||
|
im2 = Image.new("1", (1, 1), 1)
|
||||||
|
im.save(out, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
|
with Image.open(out) as reloaded:
|
||||||
|
assert reloaded.getpixel((0, 0)) == 0
|
||||||
|
|
||||||
|
reloaded.seek(1)
|
||||||
|
assert reloaded.getpixel((0, 0)) == 255
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"path, mode",
|
"path, mode",
|
||||||
(
|
(
|
||||||
|
@ -861,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
|
||||||
assert reread.info["duration"] == 8500
|
assert reread.info["duration"] == 8500
|
||||||
|
|
||||||
|
|
||||||
|
def test_loop_none(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
im = Image.new("L", (100, 100), "#000")
|
||||||
|
im.save(out, loop=None)
|
||||||
|
with Image.open(out) as reread:
|
||||||
|
assert "loop" not in reread.info
|
||||||
|
|
||||||
|
|
||||||
def test_number_of_loops(tmp_path):
|
def test_number_of_loops(tmp_path):
|
||||||
number_of_loops = 2
|
number_of_loops = 2
|
||||||
|
|
||||||
|
@ -1072,6 +1094,21 @@ def test_transparent_optimize(tmp_path):
|
||||||
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
|
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_removed_transparency(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
im = Image.new("RGB", (256, 1))
|
||||||
|
|
||||||
|
for x in range(256):
|
||||||
|
im.putpixel((x, 0), (x, 0, 0))
|
||||||
|
|
||||||
|
im.info["transparency"] = (255, 255, 255)
|
||||||
|
with pytest.warns(UserWarning):
|
||||||
|
im.save(out)
|
||||||
|
|
||||||
|
with Image.open(out) as reloaded:
|
||||||
|
assert "transparency" not in reloaded.info
|
||||||
|
|
||||||
|
|
||||||
def test_rgb_transparency(tmp_path):
|
def test_rgb_transparency(tmp_path):
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
|
@ -1087,7 +1124,8 @@ def test_rgb_transparency(tmp_path):
|
||||||
im = Image.new("RGB", (1, 1))
|
im = Image.new("RGB", (1, 1))
|
||||||
im.info["transparency"] = b""
|
im.info["transparency"] = b""
|
||||||
ims = [Image.new("RGB", (1, 1))]
|
ims = [Image.new("RGB", (1, 1))]
|
||||||
pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims)
|
with pytest.warns(UserWarning):
|
||||||
|
im.save(out, save_all=True, append_images=ims)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert "transparency" not in reloaded.info
|
assert "transparency" not in reloaded.info
|
||||||
|
@ -1115,6 +1153,18 @@ def test_bbox(tmp_path):
|
||||||
assert reread.n_frames == 2
|
assert reread.n_frames == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_bbox_alpha(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
|
||||||
|
im = Image.new("RGBA", (1, 2), (255, 0, 0, 255))
|
||||||
|
im.putpixel((0, 1), (255, 0, 0, 0))
|
||||||
|
im2 = Image.new("RGBA", (1, 2), (255, 0, 0, 0))
|
||||||
|
im.save(out, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
|
with Image.open(out) as reread:
|
||||||
|
assert reread.n_frames == 2
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_L(tmp_path):
|
def test_palette_save_L(tmp_path):
|
||||||
# Generate an L mode image with a separate palette
|
# Generate an L mode image with a separate palette
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ def test_handler(tmp_path):
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename):
|
def save(self, im, fp, filename):
|
||||||
|
|
|
@ -57,6 +57,7 @@ def test_handler(tmp_path):
|
||||||
|
|
||||||
def load(self, im):
|
def load(self, im):
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
|
im.fp.close()
|
||||||
return Image.new("RGB", (1, 1))
|
return Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
def save(self, im, fp, filename):
|
def save(self, im, fp, filename):
|
||||||
|
|
|
@ -212,12 +212,10 @@ def test_save_append_images(tmp_path):
|
||||||
def test_unexpected_size():
|
def test_unexpected_size():
|
||||||
# This image has been manually hexedited to state that it is 16x32
|
# This image has been manually hexedited to state that it is 16x32
|
||||||
# while the image within is still 16x16
|
# while the image within is still 16x16
|
||||||
def open():
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/hopper_unexpected.ico") as im:
|
with Image.open("Tests/images/hopper_unexpected.ico") as im:
|
||||||
assert im.size == (16, 16)
|
assert im.size == (16, 16)
|
||||||
|
|
||||||
pytest.warns(UserWarning, open)
|
|
||||||
|
|
||||||
|
|
||||||
def test_draw_reloaded(tmp_path):
|
def test_draw_reloaded(tmp_path):
|
||||||
with Image.open(TEST_ICO_FILE) as im:
|
with Image.open(TEST_ICO_FILE) as im:
|
||||||
|
|
|
@ -32,7 +32,8 @@ def test_unclosed_file():
|
||||||
im = Image.open(TEST_IM)
|
im = Image.open(TEST_IM)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file():
|
||||||
|
|
|
@ -214,13 +214,20 @@ class TestFileJpeg:
|
||||||
# Should not raise OSError for image with icc larger than image size.
|
# Should not raise OSError for image with icc larger than image size.
|
||||||
im.save(
|
im.save(
|
||||||
f,
|
f,
|
||||||
format="JPEG",
|
|
||||||
progressive=True,
|
progressive=True,
|
||||||
quality=95,
|
quality=95,
|
||||||
icc_profile=icc_profile,
|
icc_profile=icc_profile,
|
||||||
optimize=True,
|
optimize=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/flower2.jpg") as im:
|
||||||
|
f = str(tmp_path / "temp2.jpg")
|
||||||
|
im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955)
|
||||||
|
|
||||||
|
with Image.open("Tests/images/flower2.jpg") as im:
|
||||||
|
f = str(tmp_path / "temp3.jpg")
|
||||||
|
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
|
||||||
|
|
||||||
def test_optimize(self):
|
def test_optimize(self):
|
||||||
im1 = self.roundtrip(hopper())
|
im1 = self.roundtrip(hopper())
|
||||||
im2 = self.roundtrip(hopper(), optimize=0)
|
im2 = self.roundtrip(hopper(), optimize=0)
|
||||||
|
@ -636,12 +643,6 @@ class TestFileJpeg:
|
||||||
assert max(im2.quantization[0]) <= 255
|
assert max(im2.quantization[0]) <= 255
|
||||||
assert max(im2.quantization[1]) <= 255
|
assert max(im2.quantization[1]) <= 255
|
||||||
|
|
||||||
def test_convert_dict_qtables_deprecation(self):
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
qtable = {0: [1, 2, 3, 4]}
|
|
||||||
qtable2 = JpegImagePlugin.convert_dict_qtables(qtable)
|
|
||||||
assert qtable == qtable2
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
||||||
def test_load_djpeg(self):
|
def test_load_djpeg(self):
|
||||||
with Image.open(TEST_FILE) as img:
|
with Image.open(TEST_FILE) as img:
|
||||||
|
@ -928,6 +929,18 @@ class TestFileJpeg:
|
||||||
im.load()
|
im.load()
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||||
|
|
||||||
|
def test_repr_jpeg(self):
|
||||||
|
im = hopper()
|
||||||
|
|
||||||
|
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
|
||||||
|
assert repr_jpeg.format == "JPEG"
|
||||||
|
assert_image_similar(im, repr_jpeg, 17)
|
||||||
|
|
||||||
|
def test_repr_jpeg_error_returns_none(self):
|
||||||
|
im = hopper("F")
|
||||||
|
|
||||||
|
assert im._repr_jpeg_() is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not is_win32(), reason="Windows only")
|
@pytest.mark.skipif(not is_win32(), reason="Windows only")
|
||||||
@skip_unless_feature("jpg")
|
@skip_unless_feature("jpg")
|
||||||
|
|
|
@ -4,13 +4,21 @@ from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
|
from PIL import (
|
||||||
|
Image,
|
||||||
|
ImageFile,
|
||||||
|
Jpeg2KImagePlugin,
|
||||||
|
UnidentifiedImageError,
|
||||||
|
_binary,
|
||||||
|
features,
|
||||||
|
)
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
assert_image_similar,
|
assert_image_similar,
|
||||||
assert_image_similar_tofile,
|
assert_image_similar_tofile,
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
|
skip_unless_feature_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
EXTRA_DIR = "Tests/images/jpeg2000"
|
EXTRA_DIR = "Tests/images/jpeg2000"
|
||||||
|
@ -266,17 +274,15 @@ def test_sgnd(tmp_path):
|
||||||
assert reloaded_signed.getpixel((0, 0)) == 128
|
assert reloaded_signed.getpixel((0, 0)) == 128
|
||||||
|
|
||||||
|
|
||||||
def test_rgba():
|
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||||
|
def test_rgba(ext):
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
|
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
|
||||||
with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2:
|
# Act
|
||||||
# Act
|
im.load()
|
||||||
j2k.load()
|
|
||||||
jp2.load()
|
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert j2k.mode == "RGBA"
|
assert im.mode == "RGBA"
|
||||||
assert jp2.mode == "RGBA"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||||
|
@ -353,6 +359,35 @@ def test_subsampling_decode(name):
|
||||||
assert_image_similar(im, expected, epsilon)
|
assert_image_similar(im, expected, epsilon)
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment():
|
||||||
|
with Image.open("Tests/images/comment.jp2") as im:
|
||||||
|
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||||
|
|
||||||
|
# Test an image that is truncated partway through a codestream
|
||||||
|
with open("Tests/images/comment.jp2", "rb") as fp:
|
||||||
|
b = BytesIO(fp.read(130))
|
||||||
|
with Image.open(b) as im:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_comment():
|
||||||
|
for comment in ("Created by Pillow", b"Created by Pillow"):
|
||||||
|
out = BytesIO()
|
||||||
|
test_card.save(out, "JPEG2000", comment=comment)
|
||||||
|
|
||||||
|
with Image.open(out) as im:
|
||||||
|
assert im.info["comment"] == b"Created by Pillow"
|
||||||
|
|
||||||
|
out = BytesIO()
|
||||||
|
long_comment = b" " * 65531
|
||||||
|
test_card.save(out, "JPEG2000", comment=long_comment)
|
||||||
|
with Image.open(out) as im:
|
||||||
|
assert im.info["comment"] == long_comment
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
test_card.save(out, "JPEG2000", comment=long_comment + b" ")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file",
|
"test_file",
|
||||||
[
|
[
|
||||||
|
@ -370,3 +405,29 @@ def test_crashes(test_file):
|
||||||
im.load()
|
im.load()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature_version("jpg_2000", "2.4.0")
|
||||||
|
def test_plt_marker():
|
||||||
|
# Search the start of the codesteam for PLT
|
||||||
|
out = BytesIO()
|
||||||
|
test_card.save(out, "JPEG2000", no_jp2=True, plt=True)
|
||||||
|
out.seek(0)
|
||||||
|
while True:
|
||||||
|
marker = out.read(2)
|
||||||
|
if not marker:
|
||||||
|
assert False, "End of stream without PLT"
|
||||||
|
|
||||||
|
jp2_boxid = _binary.i16be(marker)
|
||||||
|
if jp2_boxid == 0xFF4F:
|
||||||
|
# SOC has no length
|
||||||
|
continue
|
||||||
|
elif jp2_boxid == 0xFF58:
|
||||||
|
# PLT
|
||||||
|
return
|
||||||
|
elif jp2_boxid == 0xFF93:
|
||||||
|
assert False, "SOD without finding PLT first"
|
||||||
|
|
||||||
|
hdr = out.read(2)
|
||||||
|
length = _binary.i16be(hdr)
|
||||||
|
out.seek(length - 2, os.SEEK_CUR)
|
||||||
|
|
|
@ -668,6 +668,16 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert reloaded.tag_v2[530] == (1, 1)
|
assert reloaded.tag_v2[530] == (1, 1)
|
||||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||||
|
|
||||||
|
def test_exif_ifd(self, tmp_path):
|
||||||
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
||||||
|
assert im.tag_v2[34665] == 125456
|
||||||
|
im.save(outfile)
|
||||||
|
|
||||||
|
with Image.open(outfile) as reloaded:
|
||||||
|
if Image.core.libtiff_support_custom_tags:
|
||||||
|
assert reloaded.tag_v2[34665] == 125456
|
||||||
|
|
||||||
def test_crashing_metadata(self, tmp_path):
|
def test_crashing_metadata(self, tmp_path):
|
||||||
# issue 1597
|
# issue 1597
|
||||||
with Image.open("Tests/images/rdf.tif") as im:
|
with Image.open("Tests/images/rdf.tif") as im:
|
||||||
|
@ -984,6 +994,36 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
) as im:
|
) as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
|
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file_name, mode, size, tile",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"tiff_wrong_bits_per_sample.tiff",
|
||||||
|
"RGBA",
|
||||||
|
(52, 53),
|
||||||
|
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tiff_wrong_bits_per_sample_2.tiff",
|
||||||
|
"RGB",
|
||||||
|
(16, 16),
|
||||||
|
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tiff_wrong_bits_per_sample_3.tiff",
|
||||||
|
"RGBA",
|
||||||
|
(512, 256),
|
||||||
|
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
|
||||||
|
with Image.open("Tests/images/" + file_name) as im:
|
||||||
|
assert im.mode == mode
|
||||||
|
assert im.size == size
|
||||||
|
assert im.tile == tile
|
||||||
|
im.load()
|
||||||
|
|
||||||
def test_no_rows_per_strip(self):
|
def test_no_rows_per_strip(self):
|
||||||
# This image does not have a RowsPerStrip TIFF tag
|
# This image does not have a RowsPerStrip TIFF tag
|
||||||
infile = "Tests/images/no_rows_per_strip.tif"
|
infile = "Tests/images/no_rows_per_strip.tif"
|
||||||
|
@ -1065,3 +1105,27 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
with pytest.raises(SystemError):
|
with pytest.raises(SystemError):
|
||||||
im.save(out, compression=compression)
|
im.save(out, compression=compression)
|
||||||
|
|
||||||
|
def test_save_many_compressed(self, tmp_path):
|
||||||
|
im = hopper()
|
||||||
|
out = str(tmp_path / "temp.tif")
|
||||||
|
for _ in range(10000):
|
||||||
|
im.save(out, compression="jpeg")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path, sizes",
|
||||||
|
(
|
||||||
|
("Tests/images/hopper.tif", ()),
|
||||||
|
("Tests/images/child_ifd.tiff", (16, 8)),
|
||||||
|
("Tests/images/child_ifd_jpeg.tiff", (20,)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_get_child_images(self, path, sizes):
|
||||||
|
with Image.open(path) as im:
|
||||||
|
ims = im.get_child_images()
|
||||||
|
|
||||||
|
assert len(ims) == len(sizes)
|
||||||
|
for i, im in enumerate(ims):
|
||||||
|
w = sizes[i]
|
||||||
|
expected = Image.new("RGB", (w, w), "#f00")
|
||||||
|
assert_image_similar(im, expected, 1)
|
||||||
|
|
|
@ -51,6 +51,16 @@ def test_seek():
|
||||||
assert im.tell() == 0
|
assert im.tell() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_close():
|
||||||
|
with Image.open(TEST_FILE) as im:
|
||||||
|
pass
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
im = Image.open(TEST_FILE)
|
||||||
|
im.close()
|
||||||
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
# Test an invalid OLE file
|
# Test an invalid OLE file
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
|
@ -42,7 +42,8 @@ def test_unclosed_file():
|
||||||
im = Image.open(test_files[0])
|
im = Image.open(test_files[0])
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file():
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, PdfParser, features
|
from PIL import Image, PdfParser, features
|
||||||
|
|
||||||
from .helper import hopper, mark_if_feature_version
|
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
def helper_save_as_pdf(tmp_path, mode, **kwargs):
|
def helper_save_as_pdf(tmp_path, mode, **kwargs):
|
||||||
|
@ -42,6 +42,28 @@ def test_save(tmp_path, mode):
|
||||||
helper_save_as_pdf(tmp_path, mode)
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("jpg_2000")
|
||||||
|
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
||||||
|
def test_save_alpha(tmp_path, mode):
|
||||||
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
|
def test_p_alpha(tmp_path):
|
||||||
|
# Arrange
|
||||||
|
outfile = str(tmp_path / "temp.pdf")
|
||||||
|
with Image.open("Tests/images/pil123p.png") as im:
|
||||||
|
assert im.mode == "P"
|
||||||
|
assert isinstance(im.info["transparency"], bytes)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
im.save(outfile)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
with open(outfile, "rb") as fp:
|
||||||
|
contents = fp.read()
|
||||||
|
assert b"\n/SMask " in contents
|
||||||
|
|
||||||
|
|
||||||
def test_monochrome(tmp_path):
|
def test_monochrome(tmp_path):
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = "1"
|
mode = "1"
|
||||||
|
@ -52,8 +74,8 @@ def test_monochrome(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
def test_unsupported_mode(tmp_path):
|
def test_unsupported_mode(tmp_path):
|
||||||
im = hopper("LA")
|
im = hopper("PA")
|
||||||
outfile = str(tmp_path / "temp_LA.pdf")
|
outfile = str(tmp_path / "temp_PA.pdf")
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
|
@ -79,7 +79,7 @@ class TestFilePng:
|
||||||
|
|
||||||
def test_sanity(self, tmp_path):
|
def test_sanity(self, tmp_path):
|
||||||
# internal version number
|
# internal version number
|
||||||
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
|
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
|
||||||
|
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
|
@ -532,11 +532,10 @@ class TestFilePng:
|
||||||
assert repr_png.format == "PNG"
|
assert repr_png.format == "PNG"
|
||||||
assert_image_equal(im, repr_png)
|
assert_image_equal(im, repr_png)
|
||||||
|
|
||||||
def test_repr_png_error(self):
|
def test_repr_png_error_returns_none(self):
|
||||||
im = hopper("F")
|
im = hopper("F")
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
assert im._repr_png_() is None
|
||||||
im._repr_png_()
|
|
||||||
|
|
||||||
def test_chunk_order(self, tmp_path):
|
def test_chunk_order(self, tmp_path):
|
||||||
with Image.open("Tests/images/icc_profile.png") as im:
|
with Image.open("Tests/images/icc_profile.png") as im:
|
||||||
|
|
|
@ -256,6 +256,16 @@ def test_truncated_file(tmp_path):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_enough_image_data(tmp_path):
|
||||||
|
path = str(tmp_path / "temp.ppm")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(b"P2 1 2 255 255")
|
||||||
|
|
||||||
|
with Image.open(path) as im:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("maxval", (b"0", b"65536"))
|
@pytest.mark.parametrize("maxval", (b"0", b"65536"))
|
||||||
def test_invalid_maxval(maxval, tmp_path):
|
def test_invalid_maxval(maxval, tmp_path):
|
||||||
path = str(tmp_path / "temp.ppm")
|
path = str(tmp_path / "temp.ppm")
|
||||||
|
|
|
@ -27,7 +27,8 @@ def test_unclosed_file():
|
||||||
im = Image.open(test_file)
|
im = Image.open(test_file)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file():
|
||||||
|
|
28
Tests/test_file_qoi.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PIL import Image, QoiImagePlugin
|
||||||
|
|
||||||
|
from .helper import assert_image_equal_tofile, assert_image_similar_tofile
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanity():
|
||||||
|
with Image.open("Tests/images/hopper.qoi") as im:
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
assert im.size == (128, 128)
|
||||||
|
assert im.format == "QOI"
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||||
|
|
||||||
|
with Image.open("Tests/images/pil123rgba.qoi") as im:
|
||||||
|
assert im.mode == "RGBA"
|
||||||
|
assert im.size == (162, 150)
|
||||||
|
assert im.format == "QOI"
|
||||||
|
|
||||||
|
assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_file():
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
QoiImagePlugin.QoiImageFile(invalid_file)
|
|
@ -25,7 +25,8 @@ def test_unclosed_file():
|
||||||
im = Image.open(TEST_FILE)
|
im = Image.open(TEST_FILE)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
|
|
||||||
def test_closed_file():
|
def test_closed_file():
|
||||||
|
|
|
@ -29,11 +29,9 @@ def test_sanity(codec, test_path, format):
|
||||||
|
|
||||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||||
def test_unclosed_file():
|
def test_unclosed_file():
|
||||||
def open():
|
with pytest.warns(ResourceWarning):
|
||||||
TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
|
TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
|
||||||
|
|
||||||
|
|
||||||
def test_close():
|
def test_close():
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
|
|
|
@ -163,7 +163,9 @@ def test_save_id_section(tmp_path):
|
||||||
|
|
||||||
# Save with custom id section greater than 255 characters
|
# Save with custom id section greater than 255 characters
|
||||||
id_section = b"Test content" * 25
|
id_section = b"Test content" * 25
|
||||||
pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section))
|
with pytest.warns(UserWarning):
|
||||||
|
im.save(out, id_section=id_section)
|
||||||
|
|
||||||
with Image.open(out) as test_im:
|
with Image.open(out) as test_im:
|
||||||
assert test_im.info["id_section"] == id_section[:255]
|
assert test_im.info["id_section"] == id_section[:255]
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,8 @@ class TestFileTiff:
|
||||||
im = Image.open("Tests/images/multipage.tiff")
|
im = Image.open("Tests/images/multipage.tiff")
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
pytest.warns(ResourceWarning, open)
|
with pytest.warns(ResourceWarning):
|
||||||
|
open()
|
||||||
|
|
||||||
def test_closed_file(self):
|
def test_closed_file(self):
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
|
@ -83,24 +84,6 @@ class TestFileTiff:
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"path, sizes",
|
|
||||||
(
|
|
||||||
("Tests/images/hopper.tif", ()),
|
|
||||||
("Tests/images/child_ifd.tiff", (16, 8)),
|
|
||||||
("Tests/images/child_ifd_jpeg.tiff", (20,)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_get_child_images(self, path, sizes):
|
|
||||||
with Image.open(path) as im:
|
|
||||||
ims = im.get_child_images()
|
|
||||||
|
|
||||||
assert len(ims) == len(sizes)
|
|
||||||
for i, im in enumerate(ims):
|
|
||||||
w = sizes[i]
|
|
||||||
expected = Image.new("RGB", (w, w), "#f00")
|
|
||||||
assert_image_similar(im, expected, 1)
|
|
||||||
|
|
||||||
def test_mac_tiff(self):
|
def test_mac_tiff(self):
|
||||||
# Read RGBa images from macOS [@PIL136]
|
# Read RGBa images from macOS [@PIL136]
|
||||||
|
|
||||||
|
@ -113,39 +96,16 @@ class TestFileTiff:
|
||||||
|
|
||||||
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
|
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
|
||||||
|
|
||||||
def test_bigtiff(self):
|
def test_bigtiff(self, tmp_path):
|
||||||
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||||
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||||
"file_name,mode,size,tile",
|
# multistrip support not yet implemented
|
||||||
[
|
del im.tag_v2[273]
|
||||||
(
|
|
||||||
"tiff_wrong_bits_per_sample.tiff",
|
outfile = str(tmp_path / "temp.tif")
|
||||||
"RGBA",
|
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||||
(52, 53),
|
|
||||||
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"tiff_wrong_bits_per_sample_2.tiff",
|
|
||||||
"RGB",
|
|
||||||
(16, 16),
|
|
||||||
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"tiff_wrong_bits_per_sample_3.tiff",
|
|
||||||
"RGBA",
|
|
||||||
(512, 256),
|
|
||||||
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
|
|
||||||
with Image.open("Tests/images/" + file_name) as im:
|
|
||||||
assert im.mode == mode
|
|
||||||
assert im.size == size
|
|
||||||
assert im.tile == tile
|
|
||||||
im.load()
|
|
||||||
|
|
||||||
def test_set_legacy_api(self):
|
def test_set_legacy_api(self):
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
@ -231,7 +191,8 @@ class TestFileTiff:
|
||||||
def test_bad_exif(self):
|
def test_bad_exif(self):
|
||||||
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
|
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
|
||||||
# Should not raise struct.error.
|
# Should not raise struct.error.
|
||||||
pytest.warns(UserWarning, i._getexif)
|
with pytest.warns(UserWarning):
|
||||||
|
i._getexif()
|
||||||
|
|
||||||
def test_save_rgba(self, tmp_path):
|
def test_save_rgba(self, tmp_path):
|
||||||
im = hopper("RGBA")
|
im = hopper("RGBA")
|
||||||
|
@ -244,6 +205,12 @@ class TestFileTiff:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
|
def test_8bit_s(self):
|
||||||
|
with Image.open("Tests/images/8bit.s.tif") as im:
|
||||||
|
im.load()
|
||||||
|
assert im.mode == "L"
|
||||||
|
assert im.getpixel((50, 50)) == 184
|
||||||
|
|
||||||
def test_little_endian(self):
|
def test_little_endian(self):
|
||||||
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
with Image.open("Tests/images/16bit.cropped.tif") as im:
|
||||||
assert im.getpixel((0, 0)) == 480
|
assert im.getpixel((0, 0)) == 480
|
||||||
|
|
|
@ -252,7 +252,8 @@ def test_empty_metadata():
|
||||||
head = f.read(8)
|
head = f.read(8)
|
||||||
info = TiffImagePlugin.ImageFileDirectory(head)
|
info = TiffImagePlugin.ImageFileDirectory(head)
|
||||||
# Should not raise struct.error.
|
# Should not raise struct.error.
|
||||||
pytest.warns(UserWarning, info.load, f)
|
with pytest.warns(UserWarning):
|
||||||
|
info.load(f)
|
||||||
|
|
||||||
|
|
||||||
def test_iccprofile(tmp_path):
|
def test_iccprofile(tmp_path):
|
||||||
|
@ -418,11 +419,12 @@ def test_too_many_entries():
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
|
|
||||||
# 277: ("SamplesPerPixel", SHORT, 1),
|
# 277: ("SamplesPerPixel", SHORT, 1),
|
||||||
ifd._tagdata[277] = struct.pack("hh", 4, 4)
|
ifd._tagdata[277] = struct.pack("<hh", 4, 4)
|
||||||
ifd.tagtype[277] = TiffTags.SHORT
|
ifd.tagtype[277] = TiffTags.SHORT
|
||||||
|
|
||||||
# Should not raise ValueError.
|
# Should not raise ValueError.
|
||||||
pytest.warns(UserWarning, lambda: ifd[277])
|
with pytest.warns(UserWarning):
|
||||||
|
assert ifd[277] == 4
|
||||||
|
|
||||||
|
|
||||||
def test_tag_group_data():
|
def test_tag_group_data():
|
||||||
|
|
|
@ -29,7 +29,10 @@ class TestUnsupportedWebp:
|
||||||
WebPImagePlugin.SUPPORTED = False
|
WebPImagePlugin.SUPPORTED = False
|
||||||
|
|
||||||
file_path = "Tests/images/hopper.webp"
|
file_path = "Tests/images/hopper.webp"
|
||||||
pytest.warns(UserWarning, lambda: pytest.raises(OSError, Image.open, file_path))
|
with pytest.warns(UserWarning):
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
with Image.open(file_path):
|
||||||
|
pass
|
||||||
|
|
||||||
if HAVE_WEBP:
|
if HAVE_WEBP:
|
||||||
WebPImagePlugin.SUPPORTED = True
|
WebPImagePlugin.SUPPORTED = True
|
||||||
|
@ -230,5 +233,4 @@ class TestFileWebp:
|
||||||
im.save(out_webp, save_all=True)
|
im.save(out_webp, save_all=True)
|
||||||
|
|
||||||
with Image.open(out_webp) as reloaded:
|
with Image.open(out_webp) as reloaded:
|
||||||
reloaded.load()
|
|
||||||
assert reloaded.info["duration"] == 1000
|
assert reloaded.info["duration"] == 1000
|
||||||
|
|
|
@ -134,6 +134,18 @@ def test_timestamp_and_duration(tmp_path):
|
||||||
ts += durations[frame]
|
ts += durations[frame]
|
||||||
|
|
||||||
|
|
||||||
|
def test_float_duration(tmp_path):
|
||||||
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
|
with Image.open("Tests/images/iss634.apng") as im:
|
||||||
|
assert im.info["duration"] == 70.0
|
||||||
|
|
||||||
|
im.save(temp_file, save_all=True)
|
||||||
|
|
||||||
|
with Image.open(temp_file) as reloaded:
|
||||||
|
reloaded.load()
|
||||||
|
assert reloaded.info["duration"] == 70
|
||||||
|
|
||||||
|
|
||||||
def test_seeking(tmp_path):
|
def test_seeking(tmp_path):
|
||||||
"""
|
"""
|
||||||
Create an animated WebP file, and then try seeking through frames in reverse-order,
|
Create an animated WebP file, and then try seeking through frames in reverse-order,
|
||||||
|
|
|
@ -82,9 +82,6 @@ def test_textsize(request, tmp_path):
|
||||||
assert dy == 20
|
assert dy == 20
|
||||||
assert dx in (0, 10)
|
assert dx in (0, 10)
|
||||||
assert font.getlength(chr(i)) == dx
|
assert font.getlength(chr(i)) == dx
|
||||||
with pytest.warns(DeprecationWarning) as log:
|
|
||||||
assert font.getsize(chr(i)) == (dx, dy)
|
|
||||||
assert len(log) == 1
|
|
||||||
for i in range(len(message)):
|
for i in range(len(message)):
|
||||||
msg = message[: i + 1]
|
msg = message[: i + 1]
|
||||||
assert font.getlength(msg) == len(msg) * 10
|
assert font.getlength(msg) == len(msg) * 10
|
||||||
|
|
|
@ -48,6 +48,9 @@ class TestImage:
|
||||||
"RGBX",
|
"RGBX",
|
||||||
"RGBA",
|
"RGBA",
|
||||||
"RGBa",
|
"RGBa",
|
||||||
|
"BGR;15",
|
||||||
|
"BGR;16",
|
||||||
|
"BGR;24",
|
||||||
"CMYK",
|
"CMYK",
|
||||||
"YCbCr",
|
"YCbCr",
|
||||||
"LAB",
|
"LAB",
|
||||||
|
@ -57,9 +60,7 @@ class TestImage:
|
||||||
def test_image_modes_success(self, mode):
|
def test_image_modes_success(self, mode):
|
||||||
Image.new(mode, (1, 1))
|
Image.new(mode, (1, 1))
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
||||||
"mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32")
|
|
||||||
)
|
|
||||||
def test_image_modes_fail(self, mode):
|
def test_image_modes_fail(self, mode):
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError) as e:
|
||||||
Image.new(mode, (1, 1))
|
Image.new(mode, (1, 1))
|
||||||
|
@ -134,6 +135,12 @@ class TestImage:
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
im.size = (3, 4)
|
im.size = (3, 4)
|
||||||
|
|
||||||
|
def test_set_mode(self):
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
im.mode = "P"
|
||||||
|
|
||||||
def test_invalid_image(self):
|
def test_invalid_image(self):
|
||||||
im = io.BytesIO(b"")
|
im = io.BytesIO(b"")
|
||||||
with pytest.raises(UnidentifiedImageError):
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
@ -654,15 +661,15 @@ class TestImage:
|
||||||
blank_p.palette = None
|
blank_p.palette = None
|
||||||
blank_pa.palette = None
|
blank_pa.palette = None
|
||||||
|
|
||||||
def _make_new(base_image, im, palette_result=None):
|
def _make_new(base_image, image, palette_result=None):
|
||||||
new_im = base_image._new(im)
|
new_image = base_image._new(image.im)
|
||||||
assert new_im.mode == im.mode
|
assert new_image.mode == image.mode
|
||||||
assert new_im.size == im.size
|
assert new_image.size == image.size
|
||||||
assert new_im.info == base_image.info
|
assert new_image.info == base_image.info
|
||||||
if palette_result is not None:
|
if palette_result is not None:
|
||||||
assert new_im.palette.tobytes() == palette_result.tobytes()
|
assert new_image.palette.tobytes() == palette_result.tobytes()
|
||||||
else:
|
else:
|
||||||
assert new_im.palette is None
|
assert new_image.palette is None
|
||||||
|
|
||||||
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
|
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
|
||||||
_make_new(im_p, im, None)
|
_make_new(im_p, im, None)
|
||||||
|
@ -928,25 +935,7 @@ class TestImage:
|
||||||
im.apply_transparency()
|
im.apply_transparency()
|
||||||
assert im.palette.colors[(27, 35, 6, 214)] == 24
|
assert im.palette.colors[(27, 35, 6, 214)] == 24
|
||||||
|
|
||||||
def test_categories_deprecation(self):
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert hopper().category == 0
|
|
||||||
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert Image.NORMAL == 0
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert Image.SEQUENCE == 1
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert Image.CONTAINER == 2
|
|
||||||
|
|
||||||
def test_constants(self):
|
def test_constants(self):
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert Image.LINEAR == Image.Resampling.BILINEAR
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert Image.CUBIC == Image.Resampling.BICUBIC
|
|
||||||
with pytest.warns(DeprecationWarning):
|
|
||||||
assert Image.ANTIALIAS == Image.Resampling.LANCZOS
|
|
||||||
|
|
||||||
for enum in (
|
for enum in (
|
||||||
Image.Transpose,
|
Image.Transpose,
|
||||||
Image.Transform,
|
Image.Transform,
|
||||||
|
|
|
@ -132,22 +132,26 @@ class TestImageGetPixel(AccessTest):
|
||||||
return 1
|
return 1
|
||||||
return tuple(range(1, bands + 1))
|
return tuple(range(1, bands + 1))
|
||||||
|
|
||||||
def check(self, mode, c=None):
|
def check(self, mode, expected_color=None):
|
||||||
if not c:
|
if not expected_color:
|
||||||
c = self.color(mode)
|
expected_color = self.color(mode)
|
||||||
|
|
||||||
# check putpixel
|
# check putpixel
|
||||||
im = Image.new(mode, (1, 1), None)
|
im = Image.new(mode, (1, 1), None)
|
||||||
im.putpixel((0, 0), c)
|
im.putpixel((0, 0), expected_color)
|
||||||
assert (
|
actual_color = im.getpixel((0, 0))
|
||||||
im.getpixel((0, 0)) == c
|
assert actual_color == expected_color, (
|
||||||
), f"put/getpixel roundtrip failed for mode {mode}, color {c}"
|
f"put/getpixel roundtrip failed for mode {mode}, "
|
||||||
|
f"expected {expected_color} got {actual_color}"
|
||||||
|
)
|
||||||
|
|
||||||
# check putpixel negative index
|
# check putpixel negative index
|
||||||
im.putpixel((-1, -1), c)
|
im.putpixel((-1, -1), expected_color)
|
||||||
assert (
|
actual_color = im.getpixel((-1, -1))
|
||||||
im.getpixel((-1, -1)) == c
|
assert actual_color == expected_color, (
|
||||||
), f"put/getpixel roundtrip negative index failed for mode {mode}, color {c}"
|
f"put/getpixel roundtrip negative index failed for mode {mode}, "
|
||||||
|
f"expected {expected_color} got {actual_color}"
|
||||||
|
)
|
||||||
|
|
||||||
# Check 0
|
# Check 0
|
||||||
im = Image.new(mode, (0, 0), None)
|
im = Image.new(mode, (0, 0), None)
|
||||||
|
@ -155,27 +159,32 @@ class TestImageGetPixel(AccessTest):
|
||||||
|
|
||||||
error = ValueError if self._need_cffi_access else IndexError
|
error = ValueError if self._need_cffi_access else IndexError
|
||||||
with pytest.raises(error):
|
with pytest.raises(error):
|
||||||
im.putpixel((0, 0), c)
|
im.putpixel((0, 0), expected_color)
|
||||||
with pytest.raises(error):
|
with pytest.raises(error):
|
||||||
im.getpixel((0, 0))
|
im.getpixel((0, 0))
|
||||||
# Check 0 negative index
|
# Check 0 negative index
|
||||||
with pytest.raises(error):
|
with pytest.raises(error):
|
||||||
im.putpixel((-1, -1), c)
|
im.putpixel((-1, -1), expected_color)
|
||||||
with pytest.raises(error):
|
with pytest.raises(error):
|
||||||
im.getpixel((-1, -1))
|
im.getpixel((-1, -1))
|
||||||
|
|
||||||
# check initial color
|
# check initial color
|
||||||
im = Image.new(mode, (1, 1), c)
|
im = Image.new(mode, (1, 1), expected_color)
|
||||||
assert (
|
actual_color = im.getpixel((0, 0))
|
||||||
im.getpixel((0, 0)) == c
|
assert actual_color == expected_color, (
|
||||||
), f"initial color failed for mode {mode}, color {c} "
|
f"initial color failed for mode {mode}, "
|
||||||
|
f"expected {expected_color} got {actual_color}"
|
||||||
|
)
|
||||||
|
|
||||||
# check initial color negative index
|
# check initial color negative index
|
||||||
assert (
|
actual_color = im.getpixel((-1, -1))
|
||||||
im.getpixel((-1, -1)) == c
|
assert actual_color == expected_color, (
|
||||||
), f"initial color failed with negative index for mode {mode}, color {c} "
|
f"initial color failed with negative index for mode {mode}, "
|
||||||
|
f"expected {expected_color} got {actual_color}"
|
||||||
|
)
|
||||||
|
|
||||||
# Check 0
|
# Check 0
|
||||||
im = Image.new(mode, (0, 0), c)
|
im = Image.new(mode, (0, 0), expected_color)
|
||||||
with pytest.raises(error):
|
with pytest.raises(error):
|
||||||
im.getpixel((0, 0))
|
im.getpixel((0, 0))
|
||||||
# Check 0 negative index
|
# Check 0 negative index
|
||||||
|
@ -204,14 +213,18 @@ class TestImageGetPixel(AccessTest):
|
||||||
def test_basic(self, mode):
|
def test_basic(self, mode):
|
||||||
self.check(mode)
|
self.check(mode)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
im = hopper()
|
||||||
|
assert im.getpixel([0, 0]) == (20, 20, 70)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||||
def test_signedness(self, mode):
|
@pytest.mark.parametrize(
|
||||||
|
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
|
||||||
|
)
|
||||||
|
def test_signedness(self, mode, expected_color):
|
||||||
# see https://github.com/python-pillow/Pillow/issues/452
|
# see https://github.com/python-pillow/Pillow/issues/452
|
||||||
# pixelaccess is using signed int* instead of uint*
|
# pixelaccess is using signed int* instead of uint*
|
||||||
self.check(mode, 2**15 - 1)
|
self.check(mode, expected_color)
|
||||||
self.check(mode, 2**15)
|
|
||||||
self.check(mode, 2**15 + 1)
|
|
||||||
self.check(mode, 2**16 - 1)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||||
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
||||||
|
@ -223,11 +236,13 @@ class TestImageGetPixel(AccessTest):
|
||||||
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
|
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||||
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
||||||
class TestCffiPutPixel(TestImagePutPixel):
|
class TestCffiPutPixel(TestImagePutPixel):
|
||||||
_need_cffi_access = True
|
_need_cffi_access = True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||||
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
@pytest.mark.skipif(cffi is None, reason="No CFFI")
|
||||||
class TestCffiGetPixel(TestImageGetPixel):
|
class TestCffiGetPixel(TestImageGetPixel):
|
||||||
_need_cffi_access = True
|
_need_cffi_access = True
|
||||||
|
@ -243,7 +258,8 @@ class TestCffi(AccessTest):
|
||||||
Using private interfaces, forcing a capi access and
|
Using private interfaces, forcing a capi access and
|
||||||
a pyaccess for the same image"""
|
a pyaccess for the same image"""
|
||||||
caccess = im.im.pixel_access(False)
|
caccess = im.im.pixel_access(False)
|
||||||
access = PyAccess.new(im, False)
|
with pytest.warns(DeprecationWarning):
|
||||||
|
access = PyAccess.new(im, False)
|
||||||
|
|
||||||
w, h = im.size
|
w, h = im.size
|
||||||
for x in range(0, w, 10):
|
for x in range(0, w, 10):
|
||||||
|
@ -255,26 +271,17 @@ class TestCffi(AccessTest):
|
||||||
access[(access.xsize + 1, access.ysize + 1)]
|
access[(access.xsize + 1, access.ysize + 1)]
|
||||||
|
|
||||||
def test_get_vs_c(self):
|
def test_get_vs_c(self):
|
||||||
rgb = hopper("RGB")
|
with pytest.warns(DeprecationWarning):
|
||||||
rgb.load()
|
rgb = hopper("RGB")
|
||||||
self._test_get_access(rgb)
|
rgb.load()
|
||||||
self._test_get_access(hopper("RGBA"))
|
self._test_get_access(rgb)
|
||||||
self._test_get_access(hopper("L"))
|
for mode in ("RGBA", "L", "LA", "1", "P", "F"):
|
||||||
self._test_get_access(hopper("LA"))
|
self._test_get_access(hopper(mode))
|
||||||
self._test_get_access(hopper("1"))
|
|
||||||
self._test_get_access(hopper("P"))
|
|
||||||
# self._test_get_access(hopper('PA')) # PA -- how do I make a PA image?
|
|
||||||
self._test_get_access(hopper("F"))
|
|
||||||
|
|
||||||
im = Image.new("I;16", (10, 10), 40000)
|
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
|
||||||
self._test_get_access(im)
|
im = Image.new(mode, (10, 10), 40000)
|
||||||
im = Image.new("I;16L", (10, 10), 40000)
|
self._test_get_access(im)
|
||||||
self._test_get_access(im)
|
|
||||||
im = Image.new("I;16B", (10, 10), 40000)
|
|
||||||
self._test_get_access(im)
|
|
||||||
|
|
||||||
im = Image.new("I", (10, 10), 40000)
|
|
||||||
self._test_get_access(im)
|
|
||||||
# These don't actually appear to be modes that I can actually make,
|
# These don't actually appear to be modes that I can actually make,
|
||||||
# as unpack sets them directly into the I mode.
|
# as unpack sets them directly into the I mode.
|
||||||
# im = Image.new('I;32L', (10, 10), -2**10)
|
# im = Image.new('I;32L', (10, 10), -2**10)
|
||||||
|
@ -288,7 +295,8 @@ class TestCffi(AccessTest):
|
||||||
Using private interfaces, forcing a capi access and
|
Using private interfaces, forcing a capi access and
|
||||||
a pyaccess for the same image"""
|
a pyaccess for the same image"""
|
||||||
caccess = im.im.pixel_access(False)
|
caccess = im.im.pixel_access(False)
|
||||||
access = PyAccess.new(im, False)
|
with pytest.warns(DeprecationWarning):
|
||||||
|
access = PyAccess.new(im, False)
|
||||||
|
|
||||||
w, h = im.size
|
w, h = im.size
|
||||||
for x in range(0, w, 10):
|
for x in range(0, w, 10):
|
||||||
|
@ -297,13 +305,15 @@ class TestCffi(AccessTest):
|
||||||
assert color == caccess[(x, y)]
|
assert color == caccess[(x, y)]
|
||||||
|
|
||||||
# Attempt to set the value on a read-only image
|
# Attempt to set the value on a read-only image
|
||||||
access = PyAccess.new(im, True)
|
with pytest.warns(DeprecationWarning):
|
||||||
|
access = PyAccess.new(im, True)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
access[(0, 0)] = color
|
access[(0, 0)] = color
|
||||||
|
|
||||||
def test_set_vs_c(self):
|
def test_set_vs_c(self):
|
||||||
rgb = hopper("RGB")
|
rgb = hopper("RGB")
|
||||||
rgb.load()
|
with pytest.warns(DeprecationWarning):
|
||||||
|
rgb.load()
|
||||||
self._test_set_access(rgb, (255, 128, 0))
|
self._test_set_access(rgb, (255, 128, 0))
|
||||||
self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
|
self._test_set_access(hopper("RGBA"), (255, 192, 128, 0))
|
||||||
self._test_set_access(hopper("L"), 128)
|
self._test_set_access(hopper("L"), 128)
|
||||||
|
@ -313,20 +323,16 @@ class TestCffi(AccessTest):
|
||||||
# self._test_set_access(i, (128, 128)) #PA -- undone how to make
|
# self._test_set_access(i, (128, 128)) #PA -- undone how to make
|
||||||
self._test_set_access(hopper("F"), 1024.0)
|
self._test_set_access(hopper("F"), 1024.0)
|
||||||
|
|
||||||
im = Image.new("I;16", (10, 10), 40000)
|
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
|
||||||
self._test_set_access(im, 45000)
|
im = Image.new(mode, (10, 10), 40000)
|
||||||
im = Image.new("I;16L", (10, 10), 40000)
|
self._test_set_access(im, 45000)
|
||||||
self._test_set_access(im, 45000)
|
|
||||||
im = Image.new("I;16B", (10, 10), 40000)
|
|
||||||
self._test_set_access(im, 45000)
|
|
||||||
|
|
||||||
im = Image.new("I", (10, 10), 40000)
|
|
||||||
self._test_set_access(im, 45000)
|
|
||||||
# im = Image.new('I;32L', (10, 10), -(2**10))
|
# im = Image.new('I;32L', (10, 10), -(2**10))
|
||||||
# self._test_set_access(im, -(2**13)+1)
|
# self._test_set_access(im, -(2**13)+1)
|
||||||
# im = Image.new('I;32B', (10, 10), 2**10)
|
# im = Image.new('I;32B', (10, 10), 2**10)
|
||||||
# self._test_set_access(im, 2**13-1)
|
# self._test_set_access(im, 2**13-1)
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||||
def test_not_implemented(self):
|
def test_not_implemented(self):
|
||||||
assert PyAccess.new(hopper("BGR;15")) is None
|
assert PyAccess.new(hopper("BGR;15")) is None
|
||||||
|
|
||||||
|
@ -336,7 +342,8 @@ class TestCffi(AccessTest):
|
||||||
|
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
# Do not save references to the image, only to the access object
|
# Do not save references to the image, only to the access object
|
||||||
px = Image.new("L", (size, 1), 0).load()
|
with pytest.warns(DeprecationWarning):
|
||||||
|
px = Image.new("L", (size, 1), 0).load()
|
||||||
for i in range(size):
|
for i in range(size):
|
||||||
# pixels can contain garbage if image is released
|
# pixels can contain garbage if image is released
|
||||||
assert px[i, 0] == 0
|
assert px[i, 0] == 0
|
||||||
|
@ -345,17 +352,18 @@ class TestCffi(AccessTest):
|
||||||
def test_p_putpixel_rgb_rgba(self, mode):
|
def test_p_putpixel_rgb_rgba(self, mode):
|
||||||
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
|
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
|
||||||
im = Image.new(mode, (1, 1))
|
im = Image.new(mode, (1, 1))
|
||||||
access = PyAccess.new(im, False)
|
with pytest.warns(DeprecationWarning):
|
||||||
access.putpixel((0, 0), color)
|
access = PyAccess.new(im, False)
|
||||||
|
access.putpixel((0, 0), color)
|
||||||
|
|
||||||
if len(color) == 3:
|
if len(color) == 3:
|
||||||
color += (255,)
|
color += (255,)
|
||||||
assert im.convert("RGBA").getpixel((0, 0)) == color
|
assert im.convert("RGBA").getpixel((0, 0)) == color
|
||||||
|
|
||||||
|
|
||||||
class TestImagePutPixelError(AccessTest):
|
class TestImagePutPixelError(AccessTest):
|
||||||
IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"]
|
IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"]
|
||||||
IMAGE_MODES2 = ["I", "I;16", "BGR;15"]
|
IMAGE_MODES2 = ["L", "I", "I;16"]
|
||||||
INVALID_TYPES = ["foo", 1.0, None]
|
INVALID_TYPES = ["foo", 1.0, None]
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", IMAGE_MODES1)
|
@pytest.mark.parametrize("mode", IMAGE_MODES1)
|
||||||
|
@ -370,6 +378,11 @@ class TestImagePutPixelError(AccessTest):
|
||||||
(
|
(
|
||||||
("L", (0, 2), "color must be int or single-element tuple"),
|
("L", (0, 2), "color must be int or single-element tuple"),
|
||||||
("LA", (0, 3), "color must be int, or tuple of one or two elements"),
|
("LA", (0, 3), "color must be int, or tuple of one or two elements"),
|
||||||
|
(
|
||||||
|
"BGR;15",
|
||||||
|
(0, 2),
|
||||||
|
"color must be int, or tuple of one or three elements",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"RGB",
|
"RGB",
|
||||||
(0, 2, 5),
|
(0, 2, 5),
|
||||||
|
@ -398,11 +411,6 @@ class TestImagePutPixelError(AccessTest):
|
||||||
with pytest.raises(OverflowError):
|
with pytest.raises(OverflowError):
|
||||||
im.putpixel((0, 0), 2**80)
|
im.putpixel((0, 0), 2**80)
|
||||||
|
|
||||||
def test_putpixel_unrecognized_mode(self):
|
|
||||||
im = hopper("BGR;15")
|
|
||||||
with pytest.raises(ValueError, match="unrecognized image mode"):
|
|
||||||
im.putpixel((0, 0), 0)
|
|
||||||
|
|
||||||
|
|
||||||
class TestEmbeddable:
|
class TestEmbeddable:
|
||||||
@pytest.mark.xfail(reason="failing test")
|
@pytest.mark.xfail(reason="failing test")
|
||||||
|
|
|
@ -254,17 +254,6 @@ def test_p2pa_palette():
|
||||||
assert im_pa.getpalette() == im.getpalette()
|
assert im_pa.getpalette() == im.getpalette()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
|
||||||
def test_rgb_lab(mode):
|
|
||||||
im = Image.new(mode, (1, 1))
|
|
||||||
converted_im = im.convert("LAB")
|
|
||||||
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
|
||||||
|
|
||||||
im = Image.new("LAB", (1, 1), (255, 0, 0))
|
|
||||||
converted_im = im.convert(mode)
|
|
||||||
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
|
|
||||||
|
|
||||||
|
|
||||||
def test_matrix_illegal_conversion():
|
def test_matrix_illegal_conversion():
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper("CMYK")
|
im = hopper("CMYK")
|
||||||
|
|
|
@ -4,7 +4,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
|
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
|
||||||
|
@ -42,3 +42,10 @@ def test_copy_zero():
|
||||||
out = im.copy()
|
out = im.copy()
|
||||||
assert out.mode == im.mode
|
assert out.mode == im.mode
|
||||||
assert out.size == im.size
|
assert out.size == im.size
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("libtiff")
|
||||||
|
def test_deepcopy():
|
||||||
|
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||||
|
out = copy.deepcopy(im)
|
||||||
|
assert out.size == (590, 88)
|
||||||
|
|