Merge branch 'main' into main

This commit is contained in:
Andrew Murray 2023-09-02 19:13:40 +10:00 committed by GitHub
commit cd1fec40a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
298 changed files with 4745 additions and 3547 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

@ -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,
] ]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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}")

View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

BIN
Tests/images/comment.jp2 Normal file

Binary file not shown.

Binary file not shown.

BIN
Tests/images/hopper.qoi Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

BIN
Tests/images/pil123rgba.qoi Normal file

Binary file not shown.

BIN
Tests/images/zero_width.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 B

View File

@ -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:. \

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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",
( (

View File

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

View File

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

View 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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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")

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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():

View 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():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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