Merge branch 'main' into jpeg-app-segments
|
@ -1,3 +1,10 @@
|
|||
skip_commits:
|
||||
files:
|
||||
- ".github/**/*"
|
||||
- ".gitmodules"
|
||||
- "docs/**/*"
|
||||
- "wheels/**/*"
|
||||
|
||||
version: '{build}'
|
||||
clone_folder: c:\pillow
|
||||
init:
|
||||
|
@ -6,6 +13,7 @@ init:
|
|||
# Uncomment previous line to get RDP access during the build.
|
||||
|
||||
environment:
|
||||
COVERAGE_CORE: sysmon
|
||||
EXECUTABLE: python.exe
|
||||
TEST_OPTIONS:
|
||||
DEPLOY: YES
|
||||
|
@ -13,9 +21,9 @@ environment:
|
|||
- PYTHON: C:/Python312
|
||||
ARCHITECTURE: x86
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||
- PYTHON: C:/Python38-x64
|
||||
- PYTHON: C:/Python39-x64
|
||||
ARCHITECTURE: AMD64
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
|
||||
|
||||
install:
|
||||
|
@ -24,13 +32,13 @@ install:
|
|||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||
- 7z x pillow-test-images.zip -oc:\
|
||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.0.0.20230317
|
||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||
- choco install ghostscript --version=10.3.1
|
||||
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
c:\pillow\winbuild\build\build_dep_all.cmd
|
||||
$host.SetShouldExit(0)
|
||||
- path C:\pillow\winbuild\build\bin;%PATH%
|
||||
|
|
|
@ -28,8 +28,6 @@ fi
|
|||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade wheel
|
||||
# TODO Update condition when cffi supports 3.13
|
||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install olefile
|
||||
|
@ -39,19 +37,23 @@ python3 -m pip install -U pytest-timeout
|
|||
python3 -m pip install pyroma
|
||||
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
# TODO Update condition when NumPy supports 3.13
|
||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
|
||||
# TODO Update condition when NumPy supports free-threading
|
||||
if [[ "$PYTHON_GIL" == "0" ]]; then
|
||||
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
|
||||
else
|
||||
python3 -m pip install numpy
|
||||
fi
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
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
|
||||
# TODO Update condition when pyqt6 supports free-threading
|
||||
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
|
||||
fi
|
||||
|
||||
# Pyroma uses non-isolated build and fails with old setuptools
|
||||
if [[
|
||||
$GHA_PYTHON_VERSION == pypy3.9
|
||||
|| $GHA_PYTHON_VERSION == 3.8
|
||||
|| $GHA_PYTHON_VERSION == 3.9
|
||||
]]; then
|
||||
# To match pyproject.toml
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.16.5
|
||||
cibuildwheel==2.19.2
|
||||
|
|
1
.ci/requirements-mypy.txt
Normal file
|
@ -0,0 +1 @@
|
|||
mypy==1.11.0
|
|
@ -3,12 +3,13 @@
|
|||
BasedOnStyle: Google
|
||||
AlwaysBreakAfterReturnType: All
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBraces: Attach
|
||||
ColumnLimit: 88
|
||||
DerivePointerAlignment: false
|
||||
IndentGotoLabels: false
|
||||
IndentWidth: 4
|
||||
Language: Cpp
|
||||
PointerAlignment: Right
|
||||
|
|
|
@ -19,6 +19,5 @@ exclude_also =
|
|||
[run]
|
||||
omit =
|
||||
Tests/32bit_segfault_check.py
|
||||
Tests/bench_cffi_access.py
|
||||
Tests/check_*.py
|
||||
Tests/createfontdatachunk.py
|
||||
|
|
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
|||
tidelift: "pypi/Pillow"
|
||||
tidelift: "pypi/pillow"
|
||||
|
|
15
.github/ISSUE_TEMPLATE/ISSUE_REPORT.md
vendored
|
@ -48,6 +48,21 @@ Thank you.
|
|||
* Python:
|
||||
* Pillow:
|
||||
|
||||
```text
|
||||
Please paste here the output of running:
|
||||
|
||||
python3 -m PIL.report
|
||||
or
|
||||
python3 -m PIL --report
|
||||
|
||||
Or the output of the following Python code:
|
||||
|
||||
from PIL import report
|
||||
# or
|
||||
from PIL import features
|
||||
features.pilinfo(supported_formats=False)
|
||||
```
|
||||
|
||||
<!--
|
||||
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.
|
||||
|
||||
|
|
2
.github/workflows/cifuzz.yml
vendored
|
@ -24,6 +24,8 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
Fuzzing:
|
||||
# Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+
|
||||
if: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build Fuzzers
|
||||
|
|
2
.github/workflows/docs.yml
vendored
|
@ -7,10 +7,12 @@ on:
|
|||
paths:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
- "src/PIL/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
- "src/PIL/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
13
.github/workflows/macos-install.sh
vendored
|
@ -7,16 +7,17 @@ brew install \
|
|||
ghostscript \
|
||||
libimagequant \
|
||||
libjpeg \
|
||||
libraqm \
|
||||
libtiff \
|
||||
little-cms2 \
|
||||
openjpeg \
|
||||
webp
|
||||
if [[ "$ImageOS" == "macos13" ]]; then
|
||||
brew install --ignore-dependencies libraqm
|
||||
else
|
||||
brew install libraqm
|
||||
fi
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
|
||||
# TODO Update condition when cffi supports 3.13
|
||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
|
||||
|
||||
python3 -m pip install coverage
|
||||
python3 -m pip install defusedxml
|
||||
python3 -m pip install olefile
|
||||
|
@ -24,9 +25,7 @@ python3 -m pip install -U pytest
|
|||
python3 -m pip install -U pytest-cov
|
||||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
|
||||
# TODO Update condition when NumPy supports 3.13
|
||||
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
30
.github/workflows/test-cygwin.yml
vendored
|
@ -26,13 +26,16 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-minor-version: [8, 9]
|
||||
python-minor-version: [9]
|
||||
|
||||
timeout-minutes: 40
|
||||
|
||||
|
@ -47,11 +50,12 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: egor-tensin/setup-cygwin@v4
|
||||
uses: cygwin/cygwin-install-action@v4
|
||||
with:
|
||||
packages: >
|
||||
gcc-g++
|
||||
ghostscript
|
||||
git
|
||||
ImageMagick
|
||||
jpeg
|
||||
libfreetype-devel
|
||||
|
@ -68,8 +72,6 @@ jobs:
|
|||
make
|
||||
netpbm
|
||||
perl
|
||||
python39=3.9.16-1
|
||||
python3${{ matrix.python-minor-version }}-cffi
|
||||
python3${{ matrix.python-minor-version }}-cython
|
||||
python3${{ matrix.python-minor-version }}-devel
|
||||
python3${{ matrix.python-minor-version }}-numpy
|
||||
|
@ -86,21 +88,15 @@ jobs:
|
|||
|
||||
- name: Select Python version
|
||||
run: |
|
||||
ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/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
|
||||
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
|
||||
|
||||
- name: Build system information
|
||||
run: |
|
||||
|
@ -110,11 +106,6 @@ jobs:
|
|||
run: |
|
||||
bash.exe .ci/install.sh
|
||||
|
||||
- name: Upgrade NumPy
|
||||
shell: dash.exe -l "{0}"
|
||||
run: |
|
||||
python3 -m pip install -U "numpy<1.26"
|
||||
|
||||
- name: Build
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
run: |
|
||||
|
@ -141,11 +132,12 @@ jobs:
|
|||
bash.exe .ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Cygwin
|
||||
name: Cygwin Python 3.${{ matrix.python-minor-version }}
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
22
.github/workflows/test-docker.yml
vendored
|
@ -36,32 +36,29 @@ jobs:
|
|||
docker: [
|
||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||
ubuntu-22.04-jammy-arm64v8,
|
||||
ubuntu-22.04-jammy-ppc64le,
|
||||
ubuntu-22.04-jammy-s390x,
|
||||
ubuntu-24.04-noble-ppc64le,
|
||||
ubuntu-24.04-noble-s390x,
|
||||
# Then run the remainder
|
||||
alpine,
|
||||
amazon-2-amd64,
|
||||
amazon-2023-amd64,
|
||||
arch,
|
||||
centos-7-amd64,
|
||||
centos-stream-8-amd64,
|
||||
centos-stream-9-amd64,
|
||||
debian-11-bullseye-amd64,
|
||||
debian-12-bookworm-x86,
|
||||
debian-12-bookworm-amd64,
|
||||
fedora-38-amd64,
|
||||
fedora-39-amd64,
|
||||
fedora-40-amd64,
|
||||
gentoo,
|
||||
ubuntu-20.04-focal-amd64,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
ubuntu-24.04-noble-amd64,
|
||||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-22.04-jammy-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
- docker: "ubuntu-22.04-jammy-ppc64le"
|
||||
- docker: "ubuntu-24.04-noble-ppc64le"
|
||||
qemu-arch: "ppc64le"
|
||||
- docker: "ubuntu-22.04-jammy-s390x"
|
||||
- docker: "ubuntu-24.04-noble-s390x"
|
||||
qemu-arch: "s390x"
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
@ -83,8 +80,8 @@ jobs:
|
|||
|
||||
- name: Docker build
|
||||
run: |
|
||||
# The Pillow user in the docker container is UID 1000
|
||||
sudo chown -R 1000 $GITHUB_WORKSPACE
|
||||
# The Pillow user in the docker container is UID 1001
|
||||
sudo chown -R 1001 $GITHUB_WORKSPACE
|
||||
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
sudo chown -R runner $GITHUB_WORKSPACE
|
||||
|
||||
|
@ -101,11 +98,12 @@ jobs:
|
|||
MATRIX_DOCKER: ${{ matrix.docker }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: GHA_Docker
|
||||
name: ${{ matrix.docker }}
|
||||
gcov: true
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
9
.github/workflows/test-mingw.yml
vendored
|
@ -26,6 +26,9 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
@ -61,13 +64,12 @@ jobs:
|
|||
mingw-w64-x86_64-libtiff \
|
||||
mingw-w64-x86_64-libwebp \
|
||||
mingw-w64-x86_64-openjpeg2 \
|
||||
mingw-w64-x86_64-python3-cffi \
|
||||
mingw-w64-x86_64-python3-numpy \
|
||||
mingw-w64-x86_64-python3-olefile \
|
||||
mingw-w64-x86_64-python3-pip \
|
||||
mingw-w64-x86_64-python3-setuptools \
|
||||
mingw-w64-x86_64-python-pyqt6
|
||||
|
||||
python3 -m ensurepip
|
||||
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
|
||||
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
@ -82,8 +84,9 @@ jobs:
|
|||
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: "MSYS2 MinGW"
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
|
4
.github/workflows/test-valgrind.yml
vendored
|
@ -50,7 +50,7 @@ jobs:
|
|||
|
||||
- name: Build and Run Valgrind
|
||||
run: |
|
||||
# The Pillow user in the docker container is UID 1000
|
||||
sudo chown -R 1000 $GITHUB_WORKSPACE
|
||||
# The Pillow user in the docker container is UID 1001
|
||||
sudo chown -R 1001 $GITHUB_WORKSPACE
|
||||
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
|
||||
sudo chown -R runner $GITHUB_WORKSPACE
|
||||
|
|
22
.github/workflows/test-windows.yml
vendored
|
@ -26,13 +26,16 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
|
@ -66,8 +69,16 @@ jobs:
|
|||
- name: Print build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
||||
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
|
||||
- name: Install Python dependencies
|
||||
run: >
|
||||
python3 -m pip install
|
||||
coverage>=7.4.2
|
||||
defusedxml
|
||||
olefile
|
||||
pyroma
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-timeout
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
|
@ -75,7 +86,7 @@ jobs:
|
|||
choco install nasm --no-progress
|
||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||
|
||||
choco install ghostscript --version=10.0.0.20230317 --no-progress
|
||||
choco install ghostscript --version=10.3.1 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
# Install extra test images
|
||||
|
@ -202,11 +213,12 @@ jobs:
|
|||
shell: pwsh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
name: ${{ runner.os }} Python ${{ matrix.python-version }}
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
39
.github/workflows/test.yml
vendored
|
@ -27,6 +27,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
|
@ -47,33 +48,26 @@ jobs:
|
|||
"3.11",
|
||||
"3.10",
|
||||
"3.9",
|
||||
"3.8",
|
||||
]
|
||||
include:
|
||||
- python-version: "3.11"
|
||||
PYTHONOPTIMIZE: 1
|
||||
REVERSE: "--reverse"
|
||||
- python-version: "3.10"
|
||||
PYTHONOPTIMIZE: 2
|
||||
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
|
||||
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
|
||||
# Free-threaded
|
||||
- { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
|
||||
# M1 only available for 3.10+
|
||||
- os: "macos-latest"
|
||||
python-version: "3.9"
|
||||
- os: "macos-latest"
|
||||
python-version: "3.8"
|
||||
- { os: "macos-13", python-version: "3.9" }
|
||||
exclude:
|
||||
- os: "macos-14"
|
||||
python-version: "3.9"
|
||||
- os: "macos-14"
|
||||
python-version: "3.8"
|
||||
- { os: "macos-14", python-version: "3.9" }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
if: "${{ !matrix.disable-gil }}"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
@ -82,6 +76,18 @@ jobs:
|
|||
".ci/*.sh"
|
||||
"pyproject.toml"
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
|
||||
uses: deadsnakes/action@v3.1.0
|
||||
if: "${{ matrix.disable-gil }}"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
nogil: ${{ matrix.disable-gil }}
|
||||
|
||||
- name: Set PYTHON_GIL
|
||||
if: "${{ matrix.disable-gil }}"
|
||||
run: |
|
||||
echo "PYTHON_GIL=0" >> $GITHUB_ENV
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
|
@ -149,11 +155,12 @@ jobs:
|
|||
.ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
gcov: true
|
||||
token: ${{ secrets.CODECOV_ORG_TOKEN }}
|
||||
|
||||
success:
|
||||
permissions:
|
||||
|
|
49
.github/workflows/wheels-dependencies.sh
vendored
|
@ -16,31 +16,31 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=8.3.0
|
||||
LIBPNG_VERSION=1.6.40
|
||||
JPEGTURBO_VERSION=3.0.1
|
||||
OPENJPEG_VERSION=2.5.0
|
||||
HARFBUZZ_VERSION=8.5.0
|
||||
LIBPNG_VERSION=1.6.43
|
||||
JPEGTURBO_VERSION=3.0.3
|
||||
OPENJPEG_VERSION=2.5.2
|
||||
XZ_VERSION=5.4.5
|
||||
TIFF_VERSION=4.6.0
|
||||
LCMS2_VERSION=2.16
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
GIFLIB_VERSION=5.1.4
|
||||
GIFLIB_VERSION=5.2.2
|
||||
else
|
||||
GIFLIB_VERSION=5.2.1
|
||||
fi
|
||||
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
|
||||
ZLIB_VERSION=1.3
|
||||
ZLIB_VERSION=1.3.1
|
||||
else
|
||||
ZLIB_VERSION=1.2.8
|
||||
fi
|
||||
LIBWEBP_VERSION=1.3.2
|
||||
LIBWEBP_VERSION=1.4.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.16
|
||||
LIBXCB_VERSION=1.17.0
|
||||
BROTLI_VERSION=1.1.0
|
||||
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
|
||||
function build_openjpeg {
|
||||
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
|
||||
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
|
||||
&& make install)
|
||||
|
@ -62,7 +62,7 @@ function build_brotli {
|
|||
|
||||
function build {
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
export BUILD_PREFIX="/usr/local"
|
||||
sudo chown -R runner /usr/local
|
||||
fi
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
||||
|
@ -70,13 +70,13 @@ function build {
|
|||
fi
|
||||
build_new_zlib
|
||||
|
||||
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
|
||||
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
|
||||
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
|
||||
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
|
||||
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
|
||||
fi
|
||||
else
|
||||
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||
|
@ -87,12 +87,10 @@ function build {
|
|||
build_tiff
|
||||
build_libpng
|
||||
build_lcms2
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
|
||||
cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
|
||||
done
|
||||
fi
|
||||
build_openjpeg
|
||||
if [ -f /usr/local/lib64/libopenjp2.so ]; then
|
||||
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
|
||||
fi
|
||||
|
||||
ORIGINAL_CFLAGS=$CFLAGS
|
||||
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
||||
|
@ -128,14 +126,19 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de
|
|||
untar pillow-depends-main.zip
|
||||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
|
||||
# libtiff and libxcb cause a conflict with building libtiff and libxcb
|
||||
# libxau and libxdmcp cause an issue on macOS < 11
|
||||
# if php is installed, brew tries to reinstall these after installing openblas
|
||||
# remove cairo to fix building harfbuzz on arm64
|
||||
# remove lcms2 and libpng to fix building openjpeg on arm64
|
||||
# remove zstd to avoid inclusion on x86_64
|
||||
# remove jpeg-turbo to avoid inclusion on arm64
|
||||
# remove webp and zstd to avoid inclusion on x86_64
|
||||
# curl from brew requires zstd, use system curl
|
||||
brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd
|
||||
brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
brew remove --ignore-dependencies jpeg-turbo
|
||||
else
|
||||
brew remove --ignore-dependencies webp
|
||||
fi
|
||||
|
||||
brew install pkg-config
|
||||
fi
|
||||
|
|
9
.github/workflows/wheels-test.sh
vendored
|
@ -4,13 +4,22 @@ set -e
|
|||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
brew install fribidi
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then
|
||||
sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib
|
||||
fi
|
||||
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
|
||||
apk add curl fribidi
|
||||
else
|
||||
yum install -y fribidi
|
||||
fi
|
||||
|
||||
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
|
||||
# TODO Update condition when NumPy supports free-threading
|
||||
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
|
||||
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
|
||||
else
|
||||
python3 -m pip install numpy
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "test-images-main" ]; then
|
||||
|
|
52
.github/workflows/wheels.yml
vendored
|
@ -1,10 +1,19 @@
|
|||
name: Wheels
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# │ │ │ │ │
|
||||
- cron: "42 1 * * 0,3"
|
||||
push:
|
||||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
@ -14,6 +23,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
- "winbuild/fribidi.cmake"
|
||||
|
@ -31,6 +41,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build-1-QEMU-emulated-wheels:
|
||||
if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch'
|
||||
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
@ -39,11 +50,8 @@ jobs:
|
|||
python-version:
|
||||
- pp39
|
||||
- pp310
|
||||
- cp38
|
||||
- cp39
|
||||
- cp310
|
||||
- cp311
|
||||
- cp312
|
||||
- cp3{9,10,11}
|
||||
- cp3{12,13}
|
||||
spec:
|
||||
- manylinux2014
|
||||
- manylinux_2_28
|
||||
|
@ -78,6 +86,7 @@ jobs:
|
|||
CIBW_ARCHS: "aarch64"
|
||||
# Likewise, select only one Python version per job to speed this up.
|
||||
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
|
||||
CIBW_PRERELEASE_PYTHONS: True
|
||||
# Extra options for manylinux.
|
||||
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||
|
@ -88,6 +97,7 @@ jobs:
|
|||
path: ./wheelhouse/*.whl
|
||||
|
||||
build-2-native-wheels:
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
@ -95,11 +105,11 @@ jobs:
|
|||
matrix:
|
||||
include:
|
||||
- name: "macOS x86_64"
|
||||
os: macos-latest
|
||||
os: macos-13
|
||||
cibw_arch: x86_64
|
||||
macosx_deployment_target: "10.10"
|
||||
- name: "macOS arm64"
|
||||
os: macos-latest
|
||||
os: macos-14
|
||||
cibw_arch: arm64
|
||||
macosx_deployment_target: "11.0"
|
||||
- name: "manylinux2014 and musllinux x86_64"
|
||||
|
@ -129,10 +139,10 @@ jobs:
|
|||
env:
|
||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BUILD: ${{ matrix.build }}
|
||||
CIBW_FREE_THREADED_SUPPORT: True
|
||||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_SKIP: pp38-*
|
||||
CIBW_TEST_SKIP: "*-macosx_arm64"
|
||||
CIBW_PRERELEASE_PYTHONS: True
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
@ -141,6 +151,7 @@ jobs:
|
|||
path: ./wheelhouse/*.whl
|
||||
|
||||
windows:
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
|
||||
name: Windows ${{ matrix.cibw_arch }}
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
|
@ -202,7 +213,8 @@ jobs:
|
|||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||
CIBW_CACHE_PATH: "C:\\cibw"
|
||||
CIBW_SKIP: pp38-*
|
||||
CIBW_FREE_THREADED_SUPPORT: True
|
||||
CIBW_PRERELEASE_PYTHONS: True
|
||||
CIBW_TEST_SKIP: "*-win_arm64"
|
||||
CIBW_TEST_COMMAND: 'docker run --rm
|
||||
-v {project}:C:\pillow
|
||||
|
@ -226,6 +238,7 @@ jobs:
|
|||
path: winbuild\build\bin\fribidi*
|
||||
|
||||
sdist:
|
||||
if: github.event_name != 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -244,8 +257,25 @@ jobs:
|
|||
name: dist-sdist
|
||||
path: dist/*.tar.gz
|
||||
|
||||
scientific-python-nightly-wheels-publish:
|
||||
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
needs: [build-2-native-wheels, windows]
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload wheels to scientific-python-nightly-wheels
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Upload wheels to scientific-python-nightly-wheels
|
||||
uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
|
||||
with:
|
||||
artifacts_path: dist
|
||||
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
|
||||
|
||||
pypi-publish:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload release to PyPI
|
||||
|
|
|
@ -1,35 +1,42 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.0
|
||||
rev: v0.5.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.1.1
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.7
|
||||
rev: 1.7.9
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
files: ^src/
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.4
|
||||
rev: v1.5.5
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v18.1.8
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
exclude: ^src/thirdparty/
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
|
@ -42,18 +49,25 @@ repos:
|
|||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.28.6
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 1.7.0
|
||||
rev: 2.1.3
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.16
|
||||
rev: v0.18
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
|
@ -62,5 +76,10 @@ repos:
|
|||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
- id: check-useless-excludes
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
|
|
|
@ -3,9 +3,13 @@ version: 2
|
|||
formats: [pdf]
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-lts-latest
|
||||
tools:
|
||||
python: "3"
|
||||
jobs:
|
||||
post_checkout:
|
||||
- git remote add upstream https://github.com/python-pillow/Pillow.git # For forks
|
||||
- git fetch upstream --tags
|
||||
|
||||
python:
|
||||
install:
|
||||
|
|
255
CHANGES.rst
|
@ -2,9 +2,258 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
10.3.0 (unreleased)
|
||||
11.0.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242
|
||||
[radarhere]
|
||||
|
||||
- Changed ContainerIO to subclass IO #8240
|
||||
[radarhere]
|
||||
|
||||
- Move away from APIs that use borrowed references under the free-threaded build #8216
|
||||
[hugovk, lysnikolaou]
|
||||
|
||||
- Allow size argument to resize() to be a NumPy array #8201
|
||||
[radarhere]
|
||||
|
||||
- Drop support for Python 3.8 #8183
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Add support for Python 3.13 #8181
|
||||
[hugovk, radarhere]
|
||||
|
||||
- Fix incompatibility with NumPy 1.20 #8187
|
||||
[neutrinoceros, radarhere]
|
||||
|
||||
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
|
||||
[hugovk, radarhere]
|
||||
|
||||
10.4.0 (2024-07-01)
|
||||
-------------------
|
||||
|
||||
- Raise FileNotFoundError if show_file() path does not exist #8178
|
||||
[radarhere]
|
||||
|
||||
- Improved reading 16-bit TGA images with colour #7965
|
||||
[Yay295, radarhere]
|
||||
|
||||
- Deprecate non-image ImageCms modes #8031
|
||||
[radarhere]
|
||||
|
||||
- Fixed processing multiple JPEG EXIF markers #8127
|
||||
[radarhere]
|
||||
|
||||
- Do not preserve EXIFIFD tag by default when saving TIFF images #8110
|
||||
[radarhere]
|
||||
|
||||
- Added ImageFont.load_default_imagefont() #8086
|
||||
[radarhere]
|
||||
|
||||
- Added Image.WARN_POSSIBLE_FORMATS #8063
|
||||
[radarhere]
|
||||
|
||||
- Remove zero-byte end padding when parsing any XMP data #8171
|
||||
[radarhere]
|
||||
|
||||
- Do not detect Ultra HDR images as MPO #8056
|
||||
[radarhere]
|
||||
|
||||
- Raise SyntaxError specific to JP2 #8146
|
||||
[Yay295, radarhere]
|
||||
|
||||
- Do not use first frame duration for other frames when saving APNG images #8104
|
||||
[radarhere]
|
||||
|
||||
- Consider I;16 pixel size when using a 1 mode mask #8112
|
||||
[radarhere]
|
||||
|
||||
- When saving multiple PNG frames, convert to mode rather than raw mode #8087
|
||||
[radarhere]
|
||||
|
||||
- Added byte support to FreeTypeFont #8141
|
||||
[radarhere]
|
||||
|
||||
- Allow float center for rotate operations #8114
|
||||
[radarhere]
|
||||
|
||||
- Do not read layers immediately when opening PSD images #8039
|
||||
[radarhere]
|
||||
|
||||
- Restore original thread state #8065
|
||||
[radarhere]
|
||||
|
||||
- Read IM and TIFF images as RGB, rather than RGBX #7997
|
||||
[radarhere]
|
||||
|
||||
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
|
||||
[radarhere]
|
||||
|
||||
- Clarify ImageDraw2 error message when size is missing #8165
|
||||
[radarhere]
|
||||
|
||||
- Support unpacking more rawmodes to RGBA palettes #7966
|
||||
[radarhere]
|
||||
|
||||
- Removed support for Qt 5 #8159
|
||||
[radarhere]
|
||||
|
||||
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
|
||||
[mamg22, radarhere]
|
||||
|
||||
- Improved consistency of XMP handling #8069
|
||||
[radarhere]
|
||||
|
||||
- Use pkg-config to help find libwebp and raqm #8142
|
||||
[radarhere]
|
||||
|
||||
- Accept 't' suffix for libtiff version #8126, #8129
|
||||
[radarhere]
|
||||
|
||||
- Deprecate ImageDraw.getdraw hints parameter #8124
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Added ImageDraw circle() #8085
|
||||
[void4, hugovk, radarhere]
|
||||
|
||||
- Add mypy target to Makefile #8077
|
||||
[Yay295]
|
||||
|
||||
- Added more modes to Image.MODES #7984
|
||||
[radarhere]
|
||||
|
||||
- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Fix ImagingAccess for I;16N on big-endian #7921
|
||||
[Yay295, radarhere]
|
||||
|
||||
- Support reading P mode TIFF images with padding #7996
|
||||
[radarhere]
|
||||
|
||||
- Deprecate support for libtiff < 4 #7998
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Corrected ImageShow UnixViewer command #7987
|
||||
[radarhere]
|
||||
|
||||
- Use functools.cached_property in ImageStat #7952
|
||||
[nulano, hugovk, radarhere]
|
||||
|
||||
- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956
|
||||
[Cirras, radarhere]
|
||||
|
||||
- Support reading CMYK JPEG2000 images #7947
|
||||
[radarhere]
|
||||
|
||||
10.3.0 (2024-04-01)
|
||||
-------------------
|
||||
|
||||
- CVE-2024-28219: Use ``strncpy`` to avoid buffer overflow #7928
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Deprecate ``eval()``, replacing it with ``lambda_eval()`` and ``unsafe_eval()`` #7927
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Raise ``ValueError`` if seeking to greater than offset-sized integer in TIFF #7883
|
||||
[radarhere]
|
||||
|
||||
- Add ``--report`` argument to ``__main__.py`` to omit supported formats #7818
|
||||
[nulano, radarhere, hugovk]
|
||||
|
||||
- Added RGB to I;16, I;16L, I;16B and I;16N conversion #7918, #7920
|
||||
[radarhere]
|
||||
|
||||
- Fix editable installation with custom build backend and configuration options #7658
|
||||
[nulano, radarhere]
|
||||
|
||||
- Fix putdata() for I;16N on big-endian #7209
|
||||
[Yay295, hugovk, radarhere]
|
||||
|
||||
- Determine MPO size from markers, not EXIF data #7884
|
||||
[radarhere]
|
||||
|
||||
- Improved conversion from RGB to RGBa, LA and La #7888
|
||||
[radarhere]
|
||||
|
||||
- Support FITS images with GZIP_1 compression #7894
|
||||
[radarhere]
|
||||
|
||||
- Use I;16 mode for 9-bit JPEG 2000 images #7900
|
||||
[scaramallion, radarhere]
|
||||
|
||||
- Raise ValueError if kmeans is negative #7891
|
||||
[radarhere]
|
||||
|
||||
- Remove TIFF tag OSUBFILETYPE when saving using libtiff #7893
|
||||
[radarhere]
|
||||
|
||||
- Raise ValueError for negative values when loading P1-P3 PPM images #7882
|
||||
[radarhere]
|
||||
|
||||
- Added reading of JPEG2000 palettes #7870
|
||||
[radarhere]
|
||||
|
||||
- Added alpha_quality argument when saving WebP images #7872
|
||||
[radarhere]
|
||||
|
||||
- Fixed joined corners for ImageDraw rounded_rectangle() non-integer dimensions #7881
|
||||
[radarhere]
|
||||
|
||||
- Stop reading EPS image at EOF marker #7753
|
||||
[radarhere]
|
||||
|
||||
- PSD layer co-ordinates may be negative #7706
|
||||
[radarhere]
|
||||
|
||||
- Use subprocess with CREATE_NO_WINDOW flag in ImageShow WindowsViewer #7791
|
||||
[radarhere]
|
||||
|
||||
- When saving GIF frame that restores to background color, do not fill identical pixels #7788
|
||||
[radarhere]
|
||||
|
||||
- Fixed reading PNG iCCP compression method #7823
|
||||
[radarhere]
|
||||
|
||||
- Allow writing IFDRational to UNDEFINED tag #7840
|
||||
[radarhere]
|
||||
|
||||
- Fix logged tag name when loading Exif data #7842
|
||||
[radarhere]
|
||||
|
||||
- Use maximum frame size in IHDR chunk when saving APNG images #7821
|
||||
[radarhere]
|
||||
|
||||
- Prevent opening P TGA images without a palette #7797
|
||||
[radarhere]
|
||||
|
||||
- Use palette when loading ICO images #7798
|
||||
[radarhere]
|
||||
|
||||
- Use consistent arguments for load_read and load_seek #7713
|
||||
[radarhere]
|
||||
|
||||
- Turn off nullability warnings for macOS SDK #7827
|
||||
[radarhere]
|
||||
|
||||
- Fix shift-sign issue in Convert.c #7838
|
||||
[r-barnes, radarhere]
|
||||
|
||||
- Open 16-bit grayscale PNGs as I;16 #7849
|
||||
[radarhere]
|
||||
|
||||
- Handle truncated chunks at the end of PNG images #7709
|
||||
[lajiyuan, radarhere]
|
||||
|
||||
- Match mask size to pasted image size in GifImagePlugin #7779
|
||||
[radarhere]
|
||||
|
||||
- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782
|
||||
[evanmiller, radarhere]
|
||||
|
||||
- Fixed reading FLI/FLC images with a prefix chunk #7804
|
||||
[twolife]
|
||||
|
||||
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
|
||||
[nik012003, radarhere]
|
||||
|
||||
|
@ -4241,7 +4490,7 @@ Changelog (Pillow)
|
|||
- Documentation changes, URL update, transpose, release checklist
|
||||
[radarhere]
|
||||
|
||||
- Fixed saving to nonexistant files specified by pathlib.Path objects #1748 (fixes #1747)
|
||||
- Fixed saving to nonexistent files specified by pathlib.Path objects #1748 (fixes #1747)
|
||||
[radarhere]
|
||||
|
||||
- Round Image.crop arguments to the nearest integer #1745 (fixes #1744)
|
||||
|
@ -7452,7 +7701,7 @@ The test suite includes 400 individual tests.
|
|||
- A handbook is available (distributed separately).
|
||||
|
||||
- The coordinate system is changed so that (0,0) is now located
|
||||
in the upper left corner. This is in compliancy with ISO 12087
|
||||
in the upper left corner. This is in compliance with ISO 12087
|
||||
and 90% of all other image processing and graphics libraries.
|
||||
|
||||
- Modes "1" (bilevel) and "P" (palette) have been introduced. Note
|
||||
|
|
4
LICENSE
|
@ -1,11 +1,11 @@
|
|||
The Python Imaging Library (PIL) is
|
||||
|
||||
Copyright © 1997-2011 by Secret Labs AB
|
||||
Copyright © 1995-2011 by Fredrik Lundh
|
||||
Copyright © 1995-2011 by Fredrik Lundh and contributors
|
||||
|
||||
Pillow is the friendly PIL fork. It is
|
||||
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
|
||||
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
|
||||
|
||||
Like PIL, Pillow is licensed under the open source HPND License:
|
||||
|
||||
|
|
8
Makefile
|
@ -2,7 +2,6 @@
|
|||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
python3 setup.py clean
|
||||
rm src/PIL/*.so || true
|
||||
rm -r build || true
|
||||
find . -name __pycache__ | xargs rm -r || true
|
||||
|
@ -78,8 +77,6 @@ release-test:
|
|||
python3 selftest.py
|
||||
python3 -m pytest Tests
|
||||
python3 -m pip install .
|
||||
-rm dist/*.egg
|
||||
-rmdir dist
|
||||
python3 -m pytest -qq
|
||||
python3 -m check_manifest
|
||||
python3 -m pyroma .
|
||||
|
@ -121,3 +118,8 @@ lint-fix:
|
|||
python3 -m black .
|
||||
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
|
||||
python3 -m ruff --fix .
|
||||
|
||||
.PHONY: mypy
|
||||
mypy:
|
||||
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
|
||||
python3 -m tox -e mypy
|
||||
|
|
11
README.md
|
@ -6,9 +6,9 @@
|
|||
|
||||
## Python Imaging Library (Fork)
|
||||
|
||||
Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and
|
||||
Pillow is the friendly PIL fork by [Jeffrey A. Clark and
|
||||
contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
|
||||
PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
|
||||
PIL is the Python Imaging Library by Fredrik Lundh and contributors.
|
||||
As of 2019, Pillow development is
|
||||
[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
|
||||
|
||||
|
@ -64,7 +64,7 @@ As of 2019, Pillow development is
|
|||
src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a>
|
||||
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
|
||||
alt="Tidelift"
|
||||
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
|
||||
src="https://tidelift.com/badges/package/pypi/pillow?style=flat"></a>
|
||||
<a href="https://pypi.org/project/pillow/"><img
|
||||
alt="Newest PyPI version"
|
||||
src="https://img.shields.io/pypi/v/pillow.svg"></a>
|
||||
|
@ -82,9 +82,6 @@ As of 2019, Pillow development is
|
|||
<a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img
|
||||
alt="Join the chat at https://gitter.im/python-pillow/Pillow"
|
||||
src="https://badges.gitter.im/python-pillow/Pillow.svg"></a>
|
||||
<a href="https://twitter.com/PythonPillow"><img
|
||||
alt="Follow on https://twitter.com/PythonPillow"
|
||||
src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a>
|
||||
<a href="https://fosstodon.org/@pillow"><img
|
||||
alt="Follow on https://fosstodon.org/@pillow"
|
||||
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"
|
||||
|
@ -104,7 +101,7 @@ The core image library is designed for fast access to data stored in a few basic
|
|||
## More Information
|
||||
|
||||
- [Documentation](https://pillow.readthedocs.io/)
|
||||
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
|
||||
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
|
||||
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
|
||||
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
|
||||
- [Issues](https://github.com/python-pillow/Pillow/issues)
|
||||
|
|
22
RELEASING.md
|
@ -20,8 +20,10 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
git tag 5.2.0
|
||||
git push --tags
|
||||
```
|
||||
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases).
|
||||
* [ ] 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
|
||||
|
@ -50,7 +52,9 @@ Released as needed for security, installation or critical bug fixes.
|
|||
```bash
|
||||
make sdist
|
||||
```
|
||||
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||
```bash
|
||||
git push
|
||||
|
@ -72,21 +76,17 @@ Released as needed privately to individual vendors for critical security-related
|
|||
git tag 2.5.3
|
||||
git push origin --tags
|
||||
```
|
||||
* [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
|
||||
```bash
|
||||
git push origin 2.5.x
|
||||
```
|
||||
|
||||
## Source and Binary Distributions
|
||||
|
||||
* [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
has passed, including the "Upload release to PyPI" job. This will have been triggered
|
||||
by the new tag.
|
||||
|
||||
## Publicize Release
|
||||
|
||||
* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
|
||||
* [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from PIL import PyAccess
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
# Not running this test by default. No DOS against CI.
|
||||
|
||||
|
||||
def iterate_get(size, access) -> None:
|
||||
(w, h) = size
|
||||
for x in range(w):
|
||||
for y in range(h):
|
||||
access[(x, y)]
|
||||
|
||||
|
||||
def iterate_set(size, access) -> None:
|
||||
(w, h) = size
|
||||
for x in range(w):
|
||||
for y in range(h):
|
||||
access[(x, y)] = (x % 256, y % 256, 0)
|
||||
|
||||
|
||||
def timer(func, label, *args) -> None:
|
||||
iterations = 5000
|
||||
starttime = time.time()
|
||||
for x in range(iterations):
|
||||
func(*args)
|
||||
if time.time() - starttime > 10:
|
||||
break
|
||||
endtime = time.time()
|
||||
print(
|
||||
"{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format(
|
||||
label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_direct() -> None:
|
||||
im = hopper()
|
||||
im.load()
|
||||
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
|
||||
caccess = im.im.pixel_access(False)
|
||||
access = PyAccess.new(im, False)
|
||||
|
||||
assert caccess[(0, 0)] == access[(0, 0)]
|
||||
|
||||
print(f"Size: {im.width}x{im.height}")
|
||||
timer(iterate_get, "PyAccess - get", im.size, access)
|
||||
timer(iterate_set, "PyAccess - set", im.size, access)
|
||||
timer(iterate_get, "C-api - get", im.size, caccess)
|
||||
timer(iterate_set, "C-api - set", im.size, caccess)
|
|
@ -23,7 +23,10 @@ def _get_mem_usage() -> float:
|
|||
|
||||
|
||||
def _test_leak(
|
||||
min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
|
||||
min_iterations: int,
|
||||
max_iterations: int,
|
||||
fn: Callable[..., Image.Image | None],
|
||||
*args: Any,
|
||||
) -> None:
|
||||
mem_limit = None
|
||||
for i in range(max_iterations):
|
||||
|
|
|
@ -17,6 +17,7 @@ def test_ignore_dos_text() -> None:
|
|||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
|
||||
|
@ -32,6 +33,7 @@ def test_dos_text() -> None:
|
|||
assert msg, "Decompressed Data Too Large"
|
||||
return
|
||||
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
|
||||
|
@ -57,6 +59,7 @@ def test_dos_total_memory() -> None:
|
|||
return
|
||||
|
||||
total_len = 0
|
||||
assert isinstance(im2, PngImagePlugin.PngImageFile)
|
||||
for txt in im2.text.values():
|
||||
total_len += len(txt)
|
||||
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"
|
||||
|
|
|
@ -11,13 +11,15 @@ import subprocess
|
|||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
from collections.abc import Sequence
|
||||
from functools import lru_cache
|
||||
from io import BytesIO
|
||||
from typing import Any, Callable, Sequence
|
||||
from typing import Any, Callable
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, ImageMath, features
|
||||
from PIL import Image, ImageFile, ImageMath, features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -58,9 +60,7 @@ def convert_to_comparable(
|
|||
return new_a, new_b
|
||||
|
||||
|
||||
def assert_deep_equal(
|
||||
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
|
||||
) -> None:
|
||||
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
|
||||
try:
|
||||
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
|
||||
except Exception:
|
||||
|
@ -114,7 +114,9 @@ def assert_image_similar(
|
|||
|
||||
diff = 0
|
||||
for ach, bch in zip(a.split(), b.split()):
|
||||
chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L")
|
||||
chdiff = ImageMath.lambda_eval(
|
||||
lambda args: abs(args["a"] - args["b"]), a=ach, b=bch
|
||||
).convert("L")
|
||||
diff += sum(i * num for i, num in enumerate(chdiff.histogram()))
|
||||
|
||||
ave_diff = diff / (a.size[0] * a.size[1])
|
||||
|
@ -171,12 +173,13 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
|
|||
def skip_unless_feature_version(
|
||||
feature: str, required: str, reason: str | None = None
|
||||
) -> pytest.MarkDecorator:
|
||||
if not features.check(feature):
|
||||
version = features.version(feature)
|
||||
if version is None:
|
||||
return pytest.mark.skip(f"{feature} not available")
|
||||
if reason is None:
|
||||
reason = f"{feature} is older than {required}"
|
||||
version_required = parse_version(required)
|
||||
version_available = parse_version(features.version(feature))
|
||||
version_available = parse_version(version)
|
||||
return pytest.mark.skipif(version_available < version_required, reason=reason)
|
||||
|
||||
|
||||
|
@ -186,12 +189,13 @@ def mark_if_feature_version(
|
|||
version_blacklist: str,
|
||||
reason: str | None = None,
|
||||
) -> pytest.MarkDecorator:
|
||||
if not features.check(feature):
|
||||
version = features.version(feature)
|
||||
if version is None:
|
||||
return pytest.mark.pil_noop_mark()
|
||||
if reason is None:
|
||||
reason = f"{feature} is {version_blacklist}"
|
||||
version_required = parse_version(version_blacklist)
|
||||
version_available = parse_version(features.version(feature))
|
||||
version_available = parse_version(version)
|
||||
if (
|
||||
version_available.major == version_required.major
|
||||
and version_available.minor == version_required.minor
|
||||
|
@ -217,16 +221,11 @@ class PillowLeakTestCase:
|
|||
from resource import RUSAGE_SELF, getrusage
|
||||
|
||||
mem = getrusage(RUSAGE_SELF).ru_maxrss
|
||||
if sys.platform == "darwin":
|
||||
# man 2 getrusage:
|
||||
# ru_maxrss
|
||||
# This is the maximum resident set size utilized (in bytes).
|
||||
return mem / 1024 # Kb
|
||||
# linux
|
||||
# man 2 getrusage
|
||||
# ru_maxrss (since Linux 2.6.32)
|
||||
# This is the maximum resident set size used (in kilobytes).
|
||||
return mem # Kb
|
||||
# man 2 getrusage:
|
||||
# ru_maxrss
|
||||
# This is the maximum resident set size utilized
|
||||
# in bytes on macOS, in kilobytes on Linux
|
||||
return mem / 1024 if sys.platform == "darwin" else mem
|
||||
|
||||
def _test_leak(self, core: Callable[[], None]) -> None:
|
||||
start_mem = self._get_mem_usage()
|
||||
|
@ -240,7 +239,7 @@ class PillowLeakTestCase:
|
|||
# helpers
|
||||
|
||||
|
||||
def fromstring(data: bytes) -> Image.Image:
|
||||
def fromstring(data: bytes) -> ImageFile.ImageFile:
|
||||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
|
@ -250,25 +249,38 @@ def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
|
|||
return out.getvalue()
|
||||
|
||||
|
||||
def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
|
||||
def hopper(mode: str | None = None) -> Image.Image:
|
||||
# Use caching to reduce reading from disk, but return a copy
|
||||
# so that the cached image isn't modified by the tests
|
||||
# (for fast, isolated, repeatable tests).
|
||||
|
||||
if mode is None:
|
||||
# Always return fresh not-yet-loaded version of image.
|
||||
# Operations on not-yet-loaded images is separate class of errors
|
||||
# what we should catch.
|
||||
# Operations on not-yet-loaded images are a separate class of errors
|
||||
# that we should catch.
|
||||
return Image.open("Tests/images/hopper.ppm")
|
||||
# Use caching to reduce reading from disk but so an original copy is
|
||||
# returned each time and the cached image isn't modified by tests
|
||||
# (for fast, isolated, repeatable tests).
|
||||
im = cache.get(mode)
|
||||
if im is None:
|
||||
if mode == "F":
|
||||
im = hopper("L").convert(mode)
|
||||
elif mode[:4] == "I;16":
|
||||
im = hopper("I").convert(mode)
|
||||
else:
|
||||
im = hopper().convert(mode)
|
||||
cache[mode] = im
|
||||
return im.copy()
|
||||
|
||||
return _cached_hopper(mode).copy()
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _cached_hopper(mode: str) -> Image.Image:
|
||||
if mode == "F":
|
||||
im = hopper("L")
|
||||
else:
|
||||
im = hopper()
|
||||
if mode.startswith("BGR;"):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
im = im.convert(mode)
|
||||
else:
|
||||
try:
|
||||
im = im.convert(mode)
|
||||
except ImportError:
|
||||
if mode == "LAB":
|
||||
im = Image.open("Tests/images/hopper.Lab.tif")
|
||||
else:
|
||||
raise
|
||||
return im
|
||||
|
||||
|
||||
def djpeg_available() -> bool:
|
||||
|
@ -351,7 +363,7 @@ def is_mingw() -> bool:
|
|||
|
||||
|
||||
class CachedProperty:
|
||||
def __init__(self, func: Callable[[Any], None]) -> None:
|
||||
def __init__(self, func: Callable[[Any], Any]) -> None:
|
||||
self.func = func
|
||||
|
||||
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
|
||||
|
|
BIN
Tests/icc/sGrey-v2-nano.icc
Normal file
Before Width: | Height: | Size: 578 B |
BIN
Tests/images/16_bit_binary_pgm.tiff
Normal file
BIN
Tests/images/2422.flc
Normal file
BIN
Tests/images/9bit.j2k
Normal file
BIN
Tests/images/bmp/q/rgb32h52.bmp
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
Tests/images/bmp/q/rgba32h56.bmp
Normal file
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 298 KiB |
BIN
Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff
Normal file
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 14 KiB |
BIN
Tests/images/imagedraw_polygon_width_I.tiff
Normal file
Before Width: | Height: | Size: 180 B |
BIN
Tests/images/imagedraw_rectangle_I.tiff
Normal file
BIN
Tests/images/m13.fits
Normal file
366
Tests/images/m13_gzip.fits
Normal file
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 391 B |
BIN
Tests/images/negative_top_left_layer.psd
Normal file
Before Width: | Height: | Size: 378 B After Width: | Height: | Size: 414 B |
BIN
Tests/images/p_8.tga
Normal file
BIN
Tests/images/rgba16.tga
Normal file
After Width: | Height: | Size: 48 B |
BIN
Tests/images/seek_too_large.tif
Normal file
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
BIN
Tests/images/truncated_end_chunk.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
Tests/images/ultrahdr.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
Tests/images/unknown_compression_method.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
Tests/images/unknown_mode.j2k
Normal file
|
@ -7,13 +7,14 @@ import fuzzers
|
|||
import packaging
|
||||
import pytest
|
||||
|
||||
from PIL import Image, features
|
||||
from PIL import Image, UnidentifiedImageError, features
|
||||
from Tests.helper import skip_unless_feature
|
||||
|
||||
if sys.platform.startswith("win32"):
|
||||
pytest.skip("Fuzzer is linux only", allow_module_level=True)
|
||||
if features.check("libjpeg_turbo"):
|
||||
version = packaging.version.parse(features.version("libjpeg_turbo"))
|
||||
libjpeg_turbo_version = features.version("libjpeg_turbo")
|
||||
if libjpeg_turbo_version is not None:
|
||||
version = packaging.version.parse(libjpeg_turbo_version)
|
||||
if version.major == 2 and version.minor == 0:
|
||||
pytestmark = pytest.mark.valgrind_known_error(
|
||||
reason="Known failing with libjpeg_turbo 2.0"
|
||||
|
@ -43,7 +44,7 @@ def test_fuzz_images(path: str) -> None:
|
|||
except (
|
||||
Image.DecompressionBombError,
|
||||
Image.DecompressionBombWarning,
|
||||
Image.UnidentifiedImageError,
|
||||
UnidentifiedImageError,
|
||||
):
|
||||
# Known Image.* exceptions
|
||||
assert True
|
||||
|
|
|
@ -44,6 +44,9 @@ def test_questionable() -> None:
|
|||
"pal8os2sp.bmp",
|
||||
"pal8rletrns.bmp",
|
||||
"rgb32bf-xbgr.bmp",
|
||||
"rgba32.bmp",
|
||||
"rgb32h52.bmp",
|
||||
"rgba32h56.bmp",
|
||||
]
|
||||
for f in get_files("q"):
|
||||
try:
|
||||
|
|
|
@ -321,6 +321,7 @@ class TestColorLut3DCoreAPI:
|
|||
-1, 2, 2, 2, 2, 2,
|
||||
])).load()
|
||||
# fmt: on
|
||||
assert transformed is not None
|
||||
assert transformed[0, 0] == (0, 0, 255)
|
||||
assert transformed[50, 50] == (0, 0, 255)
|
||||
assert transformed[255, 0] == (0, 255, 255)
|
||||
|
@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
|
|||
-3, 5, 5, 5, 5, 5,
|
||||
])).load()
|
||||
# fmt: on
|
||||
assert transformed is not None
|
||||
assert transformed[0, 0] == (0, 0, 255)
|
||||
assert transformed[50, 50] == (0, 0, 255)
|
||||
assert transformed[255, 0] == (0, 255, 255)
|
||||
|
@ -354,10 +356,10 @@ class TestColorLut3DCoreAPI:
|
|||
class TestColorLut3DFilter:
|
||||
def test_wrong_args(self) -> None:
|
||||
with pytest.raises(ValueError, match="should be either an integer"):
|
||||
ImageFilter.Color3DLUT("small", [1])
|
||||
ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match="should be either an integer"):
|
||||
ImageFilter.Color3DLUT((11, 11), [1])
|
||||
ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
|
||||
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
||||
|
|
|
@ -12,7 +12,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
|
|||
|
||||
|
||||
class TestDecompressionBomb:
|
||||
def teardown_method(self, method) -> None:
|
||||
def teardown_method(self) -> None:
|
||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||
|
||||
def test_no_warning_small_file(self) -> None:
|
||||
|
|
|
@ -9,9 +9,9 @@ from PIL import _deprecate
|
|||
"version, expected",
|
||||
[
|
||||
(
|
||||
11,
|
||||
"Old thing is deprecated and will be removed in Pillow 11 "
|
||||
r"\(2024-10-15\)\. Use new thing instead\.",
|
||||
12,
|
||||
"Old thing is deprecated and will be removed in Pillow 12 "
|
||||
r"\(2025-10-15\)\. Use new thing instead\.",
|
||||
),
|
||||
(
|
||||
None,
|
||||
|
@ -20,7 +20,7 @@ from PIL import _deprecate
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_version(version, expected) -> None:
|
||||
def test_version(version: int | None, expected: str) -> None:
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", version, "new thing")
|
||||
|
||||
|
@ -46,7 +46,7 @@ def test_unknown_version() -> None:
|
|||
),
|
||||
],
|
||||
)
|
||||
def test_old_version(deprecated, plural, expected) -> None:
|
||||
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
|
||||
expected = r""
|
||||
with pytest.raises(RuntimeError, match=expected):
|
||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||
|
@ -54,18 +54,18 @@ def test_old_version(deprecated, plural, expected) -> None:
|
|||
|
||||
def test_plural() -> None:
|
||||
expected = (
|
||||
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||
r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
|
||||
r"Use new thing instead\."
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
|
||||
_deprecate.deprecate("Old things", 12, "new thing", plural=True)
|
||||
|
||||
|
||||
def test_replacement_and_action() -> None:
|
||||
expected = "Use only one of 'replacement' and 'action'"
|
||||
with pytest.raises(ValueError, match=expected):
|
||||
_deprecate.deprecate(
|
||||
"Old thing", 11, replacement="new thing", action="Upgrade to new thing"
|
||||
"Old thing", 12, replacement="new thing", action="Upgrade to new thing"
|
||||
)
|
||||
|
||||
|
||||
|
@ -76,18 +76,18 @@ def test_replacement_and_action() -> None:
|
|||
"Upgrade to new thing.",
|
||||
],
|
||||
)
|
||||
def test_action(action) -> None:
|
||||
def test_action(action: str) -> None:
|
||||
expected = (
|
||||
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
|
||||
r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
|
||||
r"Upgrade to new thing\."
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", 11, action=action)
|
||||
_deprecate.deprecate("Old thing", 12, action=action)
|
||||
|
||||
|
||||
def test_no_replacement_or_action() -> None:
|
||||
expected = (
|
||||
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
|
||||
r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)"
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", 11)
|
||||
_deprecate.deprecate("Old thing", 12)
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import io
|
||||
import re
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -29,13 +30,17 @@ def test_version() -> None:
|
|||
# Check the correctness of the convenience function
|
||||
# and the format of version numbers
|
||||
|
||||
def test(name, function) -> None:
|
||||
def test(name: str, function: Callable[[str], str | None]) -> None:
|
||||
version = features.version(name)
|
||||
if not features.check(name):
|
||||
assert version is None
|
||||
else:
|
||||
assert function(name) == version
|
||||
if name != "PIL":
|
||||
if name == "zlib" and version is not None:
|
||||
version = re.sub(".zlib-ng$", "", version)
|
||||
elif name == "libtiff" and version is not None:
|
||||
version = re.sub("t$", "", version)
|
||||
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
||||
|
||||
for module in features.modules:
|
||||
|
@ -64,21 +69,25 @@ def test_webp_anim() -> None:
|
|||
|
||||
@skip_unless_feature("libjpeg_turbo")
|
||||
def test_libjpeg_turbo_version() -> None:
|
||||
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
|
||||
version = features.version("libjpeg_turbo")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||
|
||||
|
||||
@skip_unless_feature("libimagequant")
|
||||
def test_libimagequant_version() -> None:
|
||||
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
|
||||
version = features.version("libimagequant")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("feature", features.modules)
|
||||
def test_check_modules(feature) -> None:
|
||||
def test_check_modules(feature: str) -> None:
|
||||
assert features.check_module(feature) in [True, False]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("feature", features.codecs)
|
||||
def test_check_codecs(feature) -> None:
|
||||
def test_check_codecs(feature: str) -> None:
|
||||
assert features.check_codec(feature) in [True, False]
|
||||
|
||||
|
||||
|
@ -116,9 +125,10 @@ def test_unsupported_module() -> None:
|
|||
features.version_module(module)
|
||||
|
||||
|
||||
def test_pilinfo() -> None:
|
||||
@pytest.mark.parametrize("supported_formats", (True, False))
|
||||
def test_pilinfo(supported_formats: bool) -> None:
|
||||
buf = io.StringIO()
|
||||
features.pilinfo(buf)
|
||||
features.pilinfo(buf, supported_formats=supported_formats)
|
||||
out = buf.getvalue()
|
||||
lines = out.splitlines()
|
||||
assert lines[0] == "-" * 68
|
||||
|
@ -128,9 +138,15 @@ def test_pilinfo() -> None:
|
|||
while lines[0].startswith(" "):
|
||||
lines = lines[1:]
|
||||
assert lines[0] == "-" * 68
|
||||
assert lines[1].startswith("Python modules loaded from ")
|
||||
assert lines[2].startswith("Binary modules loaded from ")
|
||||
assert lines[3] == "-" * 68
|
||||
assert lines[1].startswith("Python executable is")
|
||||
lines = lines[2:]
|
||||
if lines[0].startswith("Environment Python files loaded from"):
|
||||
lines = lines[1:]
|
||||
assert lines[0].startswith("System Python files loaded from")
|
||||
assert lines[1] == "-" * 68
|
||||
assert lines[2].startswith("Python Pillow modules loaded from ")
|
||||
assert lines[3].startswith("Binary Pillow modules loaded from ")
|
||||
assert lines[4] == "-" * 68
|
||||
jpeg = (
|
||||
"\n"
|
||||
+ "-" * 68
|
||||
|
@ -141,4 +157,4 @@ def test_pilinfo() -> None:
|
|||
+ "-" * 68
|
||||
+ "\n"
|
||||
)
|
||||
assert jpeg in out
|
||||
assert supported_formats == (jpeg in out)
|
||||
|
|
|
@ -668,6 +668,16 @@ def test_apng_save_blend(tmp_path: Path) -> None:
|
|||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
||||
|
||||
def test_apng_save_size(tmp_path: Path) -> None:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
im = Image.new("L", (100, 100))
|
||||
im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert reloaded.size == (200, 200)
|
||||
|
||||
|
||||
def test_seek_after_close() -> None:
|
||||
im = Image.open("Tests/images/apng/delay.png")
|
||||
im.seek(1)
|
||||
|
@ -696,10 +706,21 @@ def test_different_modes_in_later_frames(
|
|||
assert reloaded.mode == mode
|
||||
|
||||
|
||||
def test_apng_repeated_seeks_give_correct_info() -> None:
|
||||
def test_different_durations(tmp_path: Path) -> None:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
with Image.open("Tests/images/apng/different_durations.png") as im:
|
||||
for i in range(3):
|
||||
for _ in range(3):
|
||||
im.seek(0)
|
||||
assert im.info["duration"] == 4000
|
||||
|
||||
im.seek(1)
|
||||
assert im.info["duration"] == 1000
|
||||
|
||||
im.save(test_file, save_all=True)
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert reloaded.info["duration"] == 4000
|
||||
|
||||
reloaded.seek(1)
|
||||
assert reloaded.info["duration"] == 1000
|
||||
|
|
|
@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None:
|
|||
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file) -> None:
|
||||
def test_crashes(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import BmpImagePlugin, Image
|
||||
from PIL import BmpImagePlugin, Image, _binary
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -16,7 +16,7 @@ from .helper import (
|
|||
|
||||
|
||||
def test_sanity(tmp_path: Path) -> None:
|
||||
def roundtrip(im) -> None:
|
||||
def roundtrip(im: Image.Image) -> None:
|
||||
outfile = str(tmp_path / "temp.bmp")
|
||||
|
||||
im.save(outfile, "BMP")
|
||||
|
@ -128,6 +128,29 @@ def test_load_dib() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/clipboard_target.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"header_size, path",
|
||||
(
|
||||
(12, "g/pal8os2.bmp"),
|
||||
(40, "g/pal1.bmp"),
|
||||
(52, "q/rgb32h52.bmp"),
|
||||
(56, "q/rgba32h56.bmp"),
|
||||
(64, "q/pal8os2v2.bmp"),
|
||||
(108, "g/pal8v4.bmp"),
|
||||
(124, "g/pal8v5.bmp"),
|
||||
),
|
||||
)
|
||||
def test_dib_header_size(header_size: int, path: str) -> None:
|
||||
image_path = "Tests/images/bmp/" + path
|
||||
with open(image_path, "rb") as fp:
|
||||
data = fp.read()[14:]
|
||||
assert _binary.i32le(data) == header_size
|
||||
|
||||
dib = io.BytesIO(data)
|
||||
with Image.open(dib) as im:
|
||||
im.load()
|
||||
|
||||
|
||||
def test_save_dib(tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.dib")
|
||||
|
||||
|
@ -194,7 +217,7 @@ def test_rle4() -> None:
|
|||
("Tests/images/bmp/g/pal8rle.bmp", 1064),
|
||||
),
|
||||
)
|
||||
def test_rle8_eof(file_name, length) -> None:
|
||||
def test_rle8_eof(file_name: str, length: int) -> None:
|
||||
with open(file_name, "rb") as fp:
|
||||
data = fp.read(length)
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import BufrStubImagePlugin, Image
|
||||
from PIL import BufrStubImagePlugin, Image, ImageFile
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
|
@ -50,30 +51,33 @@ def test_save(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_handler(tmp_path: Path) -> None:
|
||||
class TestHandler:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
||||
def open(self, im) -> None:
|
||||
def open(self, im: ImageFile.StubImageFile) -> None:
|
||||
self.opened = True
|
||||
|
||||
def load(self, im):
|
||||
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
|
||||
self.loaded = True
|
||||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def save(self, im, fp, filename) -> None:
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
handler = TestHandler()
|
||||
BufrStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.bufr")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -21,9 +21,23 @@ def test_isatty() -> None:
|
|||
assert container.isatty() is False
|
||||
|
||||
|
||||
def test_seek_mode_0() -> None:
|
||||
def test_seekable() -> None:
|
||||
with hopper() as im:
|
||||
container = ContainerIO.ContainerIO(im, 0, 0)
|
||||
|
||||
assert container.seekable() is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, expected_position",
|
||||
(
|
||||
(0, 33),
|
||||
(1, 66),
|
||||
(2, 100),
|
||||
),
|
||||
)
|
||||
def test_seek_mode(mode: int, expected_position: int) -> None:
|
||||
# Arrange
|
||||
mode = 0
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
|
@ -32,35 +46,15 @@ def test_seek_mode_0() -> None:
|
|||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 33
|
||||
assert container.tell() == expected_position
|
||||
|
||||
|
||||
def test_seek_mode_1() -> None:
|
||||
# Arrange
|
||||
mode = 1
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_readable(bytesmode: bool) -> None:
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
||||
# Act
|
||||
container.seek(33, mode)
|
||||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 66
|
||||
|
||||
|
||||
def test_seek_mode_2() -> None:
|
||||
# Arrange
|
||||
mode = 2
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(33, mode)
|
||||
container.seek(33, mode)
|
||||
|
||||
# Assert
|
||||
assert container.tell() == 100
|
||||
assert container.readable() is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
|
@ -70,7 +64,7 @@ def test_read_n0(bytesmode: bool) -> None:
|
|||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(81)
|
||||
assert container.seek(81) == 81
|
||||
data = container.read()
|
||||
|
||||
# Assert
|
||||
|
@ -86,7 +80,7 @@ def test_read_n(bytesmode: bool) -> None:
|
|||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(81)
|
||||
assert container.seek(81) == 81
|
||||
data = container.read(3)
|
||||
|
||||
# Assert
|
||||
|
@ -102,7 +96,7 @@ def test_read_eof(bytesmode: bool) -> None:
|
|||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
||||
# Act
|
||||
container.seek(100)
|
||||
assert container.seek(100) == 100
|
||||
data = container.read()
|
||||
|
||||
# Assert
|
||||
|
@ -113,21 +107,65 @@ def test_read_eof(bytesmode: bool) -> None:
|
|||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_readline(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
||||
# Act
|
||||
data = container.readline()
|
||||
|
||||
# Assert
|
||||
if bytesmode:
|
||||
data = data.decode()
|
||||
assert data == "This is line 1\n"
|
||||
|
||||
data = container.readline(4)
|
||||
if bytesmode:
|
||||
data = data.decode()
|
||||
assert data == "This"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_readlines(bytesmode: bool) -> None:
|
||||
expected = [
|
||||
"This is line 1\n",
|
||||
"This is line 2\n",
|
||||
"This is line 3\n",
|
||||
"This is line 4\n",
|
||||
"This is line 5\n",
|
||||
"This is line 6\n",
|
||||
"This is line 7\n",
|
||||
"This is line 8\n",
|
||||
]
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
||||
data = container.readlines()
|
||||
if bytesmode:
|
||||
data = [line.decode() for line in data]
|
||||
assert data == expected
|
||||
|
||||
assert container.seek(0) == 0
|
||||
|
||||
data = container.readlines(2)
|
||||
if bytesmode:
|
||||
data = [line.decode() for line in data]
|
||||
assert data == expected[:2]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_write(bytesmode: bool) -> None:
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
||||
assert container.writable() is False
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
container.write(b"" if bytesmode else "")
|
||||
with pytest.raises(NotImplementedError):
|
||||
container.writelines([])
|
||||
with pytest.raises(NotImplementedError):
|
||||
container.truncate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_iter(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
expected = [
|
||||
"This is line 1\n",
|
||||
|
@ -143,9 +181,21 @@ def test_readlines(bytesmode: bool) -> None:
|
|||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
||||
# Act
|
||||
data = container.readlines()
|
||||
data = []
|
||||
for line in container:
|
||||
data.append(line)
|
||||
|
||||
# Assert
|
||||
if bytesmode:
|
||||
data = [line.decode() for line in data]
|
||||
assert data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_file(bytesmode: bool) -> None:
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
||||
assert isinstance(container.fileno(), int)
|
||||
container.flush()
|
||||
container.close()
|
||||
|
|
|
@ -5,7 +5,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import EpsImagePlugin, Image, features
|
||||
from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_similar,
|
||||
|
@ -329,48 +329,6 @@ def test_read_binary_preview() -> None:
|
|||
pass
|
||||
|
||||
|
||||
def test_readline_psfile(tmp_path: Path) -> None:
|
||||
# check all the freaking line endings possible from the spec
|
||||
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
|
||||
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
||||
strings = ["something", "else", "baz", "bif"]
|
||||
|
||||
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
|
||||
ending = "Failure with line ending: %s" % (
|
||||
"".join("%s" % ord(s) for s in ending)
|
||||
)
|
||||
assert t.readline().strip("\r\n") == "something", ending
|
||||
assert t.readline().strip("\r\n") == "else", ending
|
||||
assert t.readline().strip("\r\n") == "baz", ending
|
||||
assert t.readline().strip("\r\n") == "bif", ending
|
||||
|
||||
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
|
||||
f = io.BytesIO(test_string.encode("latin-1"))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
t = EpsImagePlugin.PSFile(f)
|
||||
_test_readline(t, ending)
|
||||
|
||||
def _test_readline_file_psfile(test_string: str, ending: str) -> None:
|
||||
f = str(tmp_path / "temp.txt")
|
||||
with open(f, "wb") as w:
|
||||
w.write(test_string.encode("latin-1"))
|
||||
|
||||
with open(f, "rb") as r:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
t = EpsImagePlugin.PSFile(r)
|
||||
_test_readline(t, ending)
|
||||
|
||||
for ending in line_endings:
|
||||
s = ending.join(strings)
|
||||
_test_readline_io_psfile(s, ending)
|
||||
_test_readline_file_psfile(s, ending)
|
||||
|
||||
|
||||
def test_psfile_deprecation() -> None:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
EpsImagePlugin.PSFile(None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
|
||||
@pytest.mark.parametrize(
|
||||
"line_ending",
|
||||
|
@ -419,7 +377,7 @@ def test_emptyline() -> None:
|
|||
)
|
||||
def test_timeout(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(Image.UnidentifiedImageError):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
@ -427,9 +385,10 @@ def test_timeout(test_file: str) -> None:
|
|||
def test_bounding_box_in_trailer() -> None:
|
||||
# Check bounding boxes are parsed in the same way
|
||||
# when specified in the header and the trailer
|
||||
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
|
||||
FILE1
|
||||
) as header_image:
|
||||
with (
|
||||
Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image,
|
||||
Image.open(FILE1) as header_image,
|
||||
):
|
||||
assert trailer_image.size == header_image.size
|
||||
|
||||
|
||||
|
@ -437,3 +396,11 @@ def test_eof_before_bounding_box() -> None:
|
|||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||
pass
|
||||
|
||||
|
||||
def test_invalid_data_after_eof() -> None:
|
||||
with open("Tests/images/illuCS6_preview.eps", "rb") as f:
|
||||
img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255))
|
||||
|
||||
with Image.open(img_bytes) as img:
|
||||
assert img.mode == "RGB"
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
|
||||
from PIL import FitsImagePlugin, Image
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.fits"
|
||||
|
||||
|
@ -22,6 +22,11 @@ def test_open() -> None:
|
|||
assert_image_equal(im, hopper("L"))
|
||||
|
||||
|
||||
def test_gzip1() -> None:
|
||||
with Image.open("Tests/images/m13_gzip.fits") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/m13.fits")
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import FliImagePlugin, Image
|
||||
from PIL import FliImagePlugin, Image, ImageFile
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
||||
|
||||
|
@ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
|
|||
# save as...-> hopper.fli, default options.
|
||||
static_test_file = "Tests/images/hopper.fli"
|
||||
|
||||
# From https://samples.libav.org/fli-flc/
|
||||
# From https://samples.ffmpeg.org/fli-flc/
|
||||
animated_test_file = "Tests/images/a.fli"
|
||||
|
||||
# From https://samples.ffmpeg.org/fli-flc/
|
||||
animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
with Image.open(static_test_file) as im:
|
||||
|
@ -32,6 +35,24 @@ def test_sanity() -> None:
|
|||
assert im.is_animated
|
||||
|
||||
|
||||
def test_prefix_chunk() -> None:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
with Image.open(animated_test_file_with_prefix_chunk) as im:
|
||||
assert im.mode == "P"
|
||||
assert im.size == (320, 200)
|
||||
assert im.format == "FLI"
|
||||
assert im.info["duration"] == 171
|
||||
assert im.is_animated
|
||||
|
||||
palette = im.getpalette()
|
||||
assert palette[3:6] == [255, 255, 255]
|
||||
assert palette[381:384] == [204, 204, 12]
|
||||
assert palette[765:] == [252, 0, 0]
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Generator
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -53,6 +53,7 @@ def test_closed_file() -> None:
|
|||
|
||||
def test_seek_after_close() -> None:
|
||||
im = Image.open("Tests/images/iss634.gif")
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
|
@ -352,7 +353,7 @@ def test_palette_434(tmp_path: Path) -> None:
|
|||
|
||||
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im.copy().save(out, **kwargs)
|
||||
im.copy().save(out, "GIF", **kwargs)
|
||||
reloaded = Image.open(out)
|
||||
|
||||
return reloaded
|
||||
|
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
|
|||
img = img.convert("RGB")
|
||||
|
||||
tempfile = str(tmp_path / "temp.gif")
|
||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||
b = BytesIO()
|
||||
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||
with Image.open(tempfile) as reloaded:
|
||||
assert_image_similar(img, reloaded.convert("RGB"), 0)
|
||||
|
||||
|
@ -388,7 +390,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
|||
img = img.convert("L")
|
||||
|
||||
tempfile = str(tmp_path / "temp.gif")
|
||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
||||
b = BytesIO()
|
||||
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||
with Image.open(tempfile) as reloaded:
|
||||
assert_image_similar(img, reloaded.convert("L"), 0)
|
||||
|
||||
|
@ -647,6 +650,9 @@ def test_dispose2_palette(tmp_path: Path) -> None:
|
|||
# Center remains red every frame
|
||||
assert rgb_img.getpixel((50, 50)) == circle
|
||||
|
||||
# Check that frame transparency wasn't added unnecessarily
|
||||
assert getattr(img, "_frame_transparency") is None
|
||||
|
||||
|
||||
def test_dispose2_diff(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
@ -734,6 +740,25 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
|
|||
assert im.n_frames == 3
|
||||
|
||||
|
||||
def test_dispose2_previous_frame(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = Image.new("P", (100, 100))
|
||||
im.info["transparency"] = 0
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle([(0, 0), (100, 50)], 1)
|
||||
im.putpalette((0, 0, 0, 255, 0, 0))
|
||||
|
||||
im2 = Image.new("P", (100, 100))
|
||||
im2.putpalette((0, 0, 0))
|
||||
|
||||
im.save(out, save_all=True, append_images=[im2], disposal=[0, 2])
|
||||
|
||||
with Image.open(out) as im:
|
||||
im.seek(1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 255)
|
||||
|
||||
|
||||
def test_transparency_in_second_frame(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||
|
@ -1113,6 +1138,21 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
assert reread.n_frames == 10
|
||||
|
||||
|
||||
def test_append_different_size_image(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = Image.new("RGB", (100, 100))
|
||||
bigger_im = Image.new("RGB", (200, 200), "#f00")
|
||||
|
||||
im.save(out, save_all=True, append_images=[bigger_im])
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert reread.size == (100, 100)
|
||||
|
||||
reread.seek(1)
|
||||
assert reread.size == (100, 100)
|
||||
|
||||
|
||||
def test_transparent_optimize(tmp_path: Path) -> None:
|
||||
# From issue #2195, if the transparent color is incorrectly optimized out, GIF loses
|
||||
# transparency.
|
||||
|
@ -1215,10 +1255,11 @@ def test_palette_save_L(tmp_path: Path) -> None:
|
|||
|
||||
im = hopper("P")
|
||||
im_l = Image.frombytes("L", im.size, im.tobytes())
|
||||
palette = bytes(im.getpalette())
|
||||
palette = im.getpalette()
|
||||
assert palette is not None
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im_l.save(out, palette=palette)
|
||||
im_l.save(out, palette=bytes(palette))
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
|
||||
|
|
|
@ -5,7 +5,7 @@ from typing import IO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import GribStubImagePlugin, Image
|
||||
from PIL import GribStubImagePlugin, Image, ImageFile
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
|
@ -51,7 +51,7 @@ def test_save(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_handler(tmp_path: Path) -> None:
|
||||
class TestHandler:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
|
@ -71,10 +74,10 @@ def test_handler(tmp_path: Path) -> None:
|
|||
GribStubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.grib")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Hdf5StubImagePlugin, Image
|
||||
from PIL import Hdf5StubImagePlugin, Image, ImageFile
|
||||
|
||||
TEST_FILE = "Tests/images/hdf5.h5"
|
||||
|
||||
|
@ -41,7 +42,7 @@ def test_load() -> None:
|
|||
def test_save() -> None:
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
dummy_fp = None
|
||||
dummy_fp = BytesIO()
|
||||
dummy_filename = "dummy.filename"
|
||||
|
||||
# Act / Assert: stub cannot save without an implemented handler
|
||||
|
@ -52,7 +53,7 @@ def test_save() -> None:
|
|||
|
||||
|
||||
def test_handler(tmp_path: Path) -> None:
|
||||
class TestHandler:
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
opened = False
|
||||
loaded = False
|
||||
saved = False
|
||||
|
@ -65,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.fp.close()
|
||||
return Image.new("RGB", (1, 1))
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
return self.loaded
|
||||
|
||||
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
|
||||
self.saved = True
|
||||
|
||||
|
@ -72,10 +76,10 @@ def test_handler(tmp_path: Path) -> None:
|
|||
Hdf5StubImagePlugin.register_handler(handler)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert handler.opened
|
||||
assert not handler.loaded
|
||||
assert not handler.is_loaded()
|
||||
|
||||
im.load()
|
||||
assert handler.loaded
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.h5")
|
||||
im.save(temp_file)
|
||||
|
|
|
@ -38,6 +38,17 @@ def test_black_and_white() -> None:
|
|||
assert im.size == (16, 16)
|
||||
|
||||
|
||||
def test_palette(tmp_path: Path) -> None:
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
|
||||
im = Image.new("P", (16, 16))
|
||||
im.save(temp_file)
|
||||
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert reloaded.mode == "P"
|
||||
assert reloaded.palette is not None
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||
with pytest.raises(SyntaxError):
|
||||
|
@ -135,7 +146,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
|
||||
def test_save_to_bytes_bmp(mode) -> None:
|
||||
def test_save_to_bytes_bmp(mode: str) -> None:
|
||||
output = io.BytesIO()
|
||||
im = hopper(mode)
|
||||
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
|
||||
|
|
|
@ -82,7 +82,7 @@ def test_eoferror() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
||||
def test_roundtrip(mode, tmp_path: Path) -> None:
|
||||
def test_roundtrip(mode: str, tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.im")
|
||||
im = hopper(mode)
|
||||
im.save(out)
|
||||
|
|
|
@ -57,6 +57,7 @@ def test_getiptcinfo_fotostation() -> None:
|
|||
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||
|
||||
# Assert
|
||||
assert iptc is not None
|
||||
for tag in iptc.keys():
|
||||
if tag[0] == 240:
|
||||
return
|
||||
|
@ -98,7 +99,7 @@ def test_i() -> None:
|
|||
assert ret == 97
|
||||
|
||||
|
||||
def test_dump(monkeypatch) -> None:
|
||||
def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
c = b"abc"
|
||||
# Temporarily redirect stdout
|
||||
|
|
|
@ -6,7 +6,7 @@ import warnings
|
|||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -45,14 +45,20 @@ TEST_FILE = "Tests/images/hopper.jpg"
|
|||
|
||||
@skip_unless_feature("jpg")
|
||||
class TestFileJpeg:
|
||||
def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
|
||||
def roundtrip_with_bytes(
|
||||
self, im: Image.Image, **options: Any
|
||||
) -> tuple[JpegImagePlugin.JpegImageFile, int]:
|
||||
out = BytesIO()
|
||||
im.save(out, "JPEG", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
im = Image.open(out)
|
||||
im.bytes = test_bytes # for testing only
|
||||
return im
|
||||
reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out))
|
||||
return reloaded, test_bytes
|
||||
|
||||
def roundtrip(
|
||||
self, im: Image.Image, **options: Any
|
||||
) -> JpegImagePlugin.JpegImageFile:
|
||||
return self.roundtrip_with_bytes(im, **options)[0]
|
||||
|
||||
def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image:
|
||||
"""Generates a very hard to compress file
|
||||
|
@ -64,7 +70,9 @@ class TestFileJpeg:
|
|||
|
||||
def test_sanity(self) -> None:
|
||||
# internal version number
|
||||
assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
|
||||
version = features.version_codec("jpg")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+$", version)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.load()
|
||||
|
@ -171,7 +179,7 @@ class TestFileJpeg:
|
|||
assert k > 0.9
|
||||
|
||||
def test_rgb(self) -> None:
|
||||
def getchannels(im: Image.Image) -> tuple[int, int, int]:
|
||||
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
|
||||
return tuple(v[0] for v in im.layer)
|
||||
|
||||
im = hopper()
|
||||
|
@ -188,7 +196,7 @@ class TestFileJpeg:
|
|||
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
||||
)
|
||||
def test_dpi(self, test_image_path: str) -> None:
|
||||
def test(xdpi: int, ydpi: int | None = None):
|
||||
def test(xdpi: int, ydpi: int | None = None) -> tuple[int, int] | None:
|
||||
with Image.open(test_image_path) as im:
|
||||
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
|
||||
return im.info.get("dpi")
|
||||
|
@ -271,13 +279,13 @@ class TestFileJpeg:
|
|||
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
|
||||
|
||||
def test_optimize(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im2 = self.roundtrip(hopper(), optimize=0)
|
||||
im3 = self.roundtrip(hopper(), optimize=1)
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0)
|
||||
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1)
|
||||
assert_image_equal(im1, im2)
|
||||
assert_image_equal(im1, im3)
|
||||
assert im1.bytes >= im2.bytes
|
||||
assert im1.bytes >= im3.bytes
|
||||
assert im1_bytes >= im2_bytes
|
||||
assert im1_bytes >= im3_bytes
|
||||
|
||||
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
|
||||
# https://github.com/python-pillow/Pillow/issues/148
|
||||
|
@ -287,15 +295,15 @@ class TestFileJpeg:
|
|||
im.save(f, format="JPEG", optimize=True)
|
||||
|
||||
def test_progressive(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2 = self.roundtrip(hopper(), progressive=False)
|
||||
im3 = self.roundtrip(hopper(), progressive=True)
|
||||
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True)
|
||||
assert not im1.info.get("progressive")
|
||||
assert not im2.info.get("progressive")
|
||||
assert im3.info.get("progressive")
|
||||
|
||||
assert_image_equal(im1, im3)
|
||||
assert im1.bytes >= im3.bytes
|
||||
assert im1_bytes >= im3_bytes
|
||||
|
||||
def test_progressive_large_buffer(self, tmp_path: Path) -> None:
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
|
@ -366,6 +374,7 @@ class TestFileJpeg:
|
|||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
transposed = ImageOps.exif_transpose(im)
|
||||
assert transposed is not None
|
||||
exif = transposed.getexif()
|
||||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
|
@ -444,14 +453,14 @@ class TestFileJpeg:
|
|||
assert im3.info.get("progression")
|
||||
|
||||
def test_quality(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
im2 = self.roundtrip(hopper(), quality=50)
|
||||
im1, im1_bytes = self.roundtrip_with_bytes(hopper())
|
||||
im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50)
|
||||
assert_image(im1, im2.mode, im2.size)
|
||||
assert im1.bytes >= im2.bytes
|
||||
assert im1_bytes >= im2_bytes
|
||||
|
||||
im3 = self.roundtrip(hopper(), quality=0)
|
||||
im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0)
|
||||
assert_image(im1, im3.mode, im3.size)
|
||||
assert im2.bytes > im3.bytes
|
||||
assert im2_bytes > im3_bytes
|
||||
|
||||
def test_smooth(self) -> None:
|
||||
im1 = self.roundtrip(hopper())
|
||||
|
@ -459,7 +468,9 @@ class TestFileJpeg:
|
|||
assert_image(im1, im2.mode, im2.size)
|
||||
|
||||
def test_subsampling(self) -> None:
|
||||
def getsampling(im: Image.Image):
|
||||
def getsampling(
|
||||
im: JpegImagePlugin.JpegImageFile,
|
||||
) -> tuple[int, int, int, int, int, int]:
|
||||
layer = im.layer
|
||||
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
|
||||
|
||||
|
@ -715,7 +726,7 @@ class TestFileJpeg:
|
|||
def test_save_cjpeg(self, tmp_path: Path) -> None:
|
||||
with Image.open(TEST_FILE) as img:
|
||||
tempfile = str(tmp_path / "temp.jpg")
|
||||
JpegImagePlugin._save_cjpeg(img, 0, tempfile)
|
||||
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
|
||||
# Default save quality is 75%, so a tiny bit of difference is alright
|
||||
assert_image_similar_tofile(img, tempfile, 17)
|
||||
|
||||
|
@ -886,7 +897,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_multiple_exif(self) -> None:
|
||||
with Image.open("Tests/images/multiple_exif.jpg") as im:
|
||||
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
|
||||
assert im.getexif()[270] == "firstsecond"
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
|
@ -933,24 +944,25 @@ class TestFileJpeg:
|
|||
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
|
||||
assert im.info["icc_profile"] == b"profile"
|
||||
|
||||
def test_jpeg_magic_number(self) -> None:
|
||||
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
size = 4097
|
||||
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
|
||||
buffer.max_pos = 0
|
||||
max_pos = 0
|
||||
orig_read = buffer.read
|
||||
|
||||
def read(n=-1):
|
||||
def read(n: int | None = -1) -> bytes:
|
||||
nonlocal max_pos
|
||||
res = orig_read(n)
|
||||
buffer.max_pos = max(buffer.max_pos, buffer.tell())
|
||||
max_pos = max(max_pos, buffer.tell())
|
||||
return res
|
||||
|
||||
buffer.read = read
|
||||
monkeypatch.setattr(buffer, "read", read)
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(buffer):
|
||||
pass
|
||||
|
||||
# Assert the entire file has not been read
|
||||
assert 0 < buffer.max_pos < size
|
||||
assert 0 < max_pos < size
|
||||
|
||||
def test_getxmp(self) -> None:
|
||||
with Image.open("Tests/images/xmp_test.jpg") as im:
|
||||
|
@ -961,6 +973,7 @@ class TestFileJpeg:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
@ -1011,13 +1024,7 @@ class TestFileJpeg:
|
|||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
return 0, 0
|
||||
|
||||
decoder = InfiniteMockPyDecoder(None)
|
||||
|
||||
def closure(mode: str, *args) -> InfiniteMockPyDecoder:
|
||||
decoder.__init__(mode, *args)
|
||||
return decoder
|
||||
|
||||
Image.register_decoder("INFINITE", closure)
|
||||
Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.tile = [
|
||||
|
@ -1051,8 +1058,10 @@ class TestFileJpeg:
|
|||
|
||||
def test_repr_jpeg(self) -> None:
|
||||
im = hopper()
|
||||
b = im._repr_jpeg_()
|
||||
assert b is not None
|
||||
|
||||
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
|
||||
with Image.open(BytesIO(b)) as repr_jpeg:
|
||||
assert repr_jpeg.format == "JPEG"
|
||||
assert_image_similar(im, repr_jpeg, 17)
|
||||
|
||||
|
|
|
@ -40,17 +40,17 @@ test_card.load()
|
|||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
out = BytesIO()
|
||||
im.save(out, "JPEG2000", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
with Image.open(out) as im:
|
||||
im.bytes = test_bytes # for testing only
|
||||
im.load()
|
||||
return im
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
# Internal version number
|
||||
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000"))
|
||||
version = features.version_codec("jpg_2000")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||
|
||||
with Image.open("Tests/images/test-card-lossless.jp2") as im:
|
||||
px = im.load()
|
||||
|
@ -77,7 +77,9 @@ def test_invalid_file() -> None:
|
|||
def test_bytesio() -> None:
|
||||
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
|
||||
data = BytesIO(f.read())
|
||||
assert_image_similar_tofile(test_card, data, 1.0e-3)
|
||||
with Image.open(data) as im:
|
||||
im.load()
|
||||
assert_image_similar(im, test_card, 1.0e-3)
|
||||
|
||||
|
||||
# These two test pre-written JPEG 2000 files that were not written with
|
||||
|
@ -289,6 +291,16 @@ def test_rgba(ext: str) -> None:
|
|||
assert im.mode == "RGBA"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
@skip_unless_feature_version("jpg_2000", "2.5.1")
|
||||
def test_cmyk() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im:
|
||||
assert im.mode == "CMYK"
|
||||
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
|
||||
with Image.open("Tests/images/16bit.cropped" + ext) as im:
|
||||
|
@ -323,9 +335,15 @@ def test_issue_6194() -> None:
|
|||
assert im.getpixel((5, 5)) == 31
|
||||
|
||||
|
||||
def test_unknown_j2k_mode() -> None:
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open("Tests/images/unknown_mode.j2k"):
|
||||
pass
|
||||
|
||||
|
||||
def test_unbound_local() -> None:
|
||||
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
||||
with pytest.raises(OSError):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open("Tests/images/unbound_variable.jp2"):
|
||||
pass
|
||||
|
||||
|
@ -340,6 +358,7 @@ def test_parser_feed() -> None:
|
|||
p.feed(data)
|
||||
|
||||
# Assert
|
||||
assert p.image is not None
|
||||
assert p.image.size == (640, 480)
|
||||
|
||||
|
||||
|
@ -363,6 +382,16 @@ def test_subsampling_decode(name: str) -> None:
|
|||
assert_image_similar(im, expected, epsilon)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
def test_pclr() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
|
||||
assert im.mode == "P"
|
||||
assert len(im.palette.colors) == 256
|
||||
assert im.palette.colors[(255, 255, 255)] == 0
|
||||
|
||||
|
||||
def test_comment() -> None:
|
||||
with Image.open("Tests/images/comment.jp2") as im:
|
||||
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||
|
@ -435,3 +464,9 @@ def test_plt_marker() -> None:
|
|||
hdr = out.read(2)
|
||||
length = _binary.i16be(hdr)
|
||||
out.seek(length - 2, os.SEEK_CUR)
|
||||
|
||||
|
||||
def test_9bit() -> None:
|
||||
with Image.open("Tests/images/9bit.j2k") as im:
|
||||
assert im.mode == "I;16"
|
||||
assert im.size == (128, 128)
|
||||
|
|
|
@ -6,13 +6,13 @@ import itertools
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
|
||||
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -27,7 +27,7 @@ from .helper import (
|
|||
|
||||
@skip_unless_feature("libtiff")
|
||||
class LibTiffTestCase:
|
||||
def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None:
|
||||
def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None:
|
||||
"""Helper tests that assert basic sanity about the g4 tiff reading"""
|
||||
# 1 bit
|
||||
assert im.mode == "1"
|
||||
|
@ -52,7 +52,9 @@ class LibTiffTestCase:
|
|||
|
||||
class TestFileLibTiff(LibTiffTestCase):
|
||||
def test_version(self) -> None:
|
||||
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff"))
|
||||
version = features.version_codec("libtiff")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+\.\d+t?$", version)
|
||||
|
||||
def test_g4_tiff(self, tmp_path: Path) -> None:
|
||||
"""Test the ordinary file path load path"""
|
||||
|
@ -90,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
|
||||
"""Testing loading from non-disk non-BytesIO file object"""
|
||||
test_file = "Tests/images/hopper_g4_500.tif"
|
||||
s = io.BytesIO()
|
||||
with open(test_file, "rb") as f:
|
||||
s.write(f.read())
|
||||
s.seek(0)
|
||||
r = io.BufferedReader(s)
|
||||
data = f.read()
|
||||
|
||||
class NonBytesIO(io.RawIOBase):
|
||||
def read(self, size: int = -1) -> bytes:
|
||||
nonlocal data
|
||||
if size == -1:
|
||||
size = len(data)
|
||||
result = data[:size]
|
||||
data = data[size:]
|
||||
return result
|
||||
|
||||
def readable(self) -> bool:
|
||||
return True
|
||||
|
||||
r = io.BufferedReader(NonBytesIO())
|
||||
with Image.open(r) as im:
|
||||
assert im.size == (500, 500)
|
||||
self._assert_noerr(tmp_path, im)
|
||||
|
@ -185,7 +198,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert field in reloaded, f"{field} not in metadata"
|
||||
|
||||
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
|
||||
def test_additional_metadata(self, tmp_path: Path) -> None:
|
||||
def test_additional_metadata(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
# these should not crash. Seriously dummy data, most of it doesn't make
|
||||
# any sense, so we're running up against limits where we're asking
|
||||
# libtiff to do stupid things.
|
||||
|
@ -225,9 +240,10 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
new_ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
for tag, info in core_items.items():
|
||||
assert info.type is not None
|
||||
if info.length == 1:
|
||||
new_ifd[tag] = values[info.type]
|
||||
if info.length == 0:
|
||||
elif not info.length:
|
||||
new_ifd[tag] = tuple(values[info.type] for _ in range(3))
|
||||
else:
|
||||
new_ifd[tag] = tuple(values[info.type] for _ in range(info.length))
|
||||
|
@ -236,94 +252,109 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
del new_ifd[338]
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
im.save(out, tiffinfo=new_ifd)
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
@pytest.mark.parametrize(
|
||||
"libtiff",
|
||||
(
|
||||
pytest.param(
|
||||
True,
|
||||
marks=pytest.mark.skipif(
|
||||
not getattr(Image.core, "libtiff_support_custom_tags", False),
|
||||
reason="Custom tags not supported by older libtiff",
|
||||
),
|
||||
),
|
||||
False,
|
||||
),
|
||||
)
|
||||
def test_custom_metadata(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||
|
||||
class Tc(NamedTuple):
|
||||
value: Any
|
||||
type: int
|
||||
supported_by_default: bool
|
||||
|
||||
def test_custom_metadata(self, tmp_path: Path) -> None:
|
||||
tc = namedtuple("tc", "value,type,supported_by_default")
|
||||
custom = {
|
||||
37000 + k: v
|
||||
for k, v in enumerate(
|
||||
[
|
||||
tc(4, TiffTags.SHORT, True),
|
||||
tc(123456789, TiffTags.LONG, True),
|
||||
tc(-4, TiffTags.SIGNED_BYTE, False),
|
||||
tc(-4, TiffTags.SIGNED_SHORT, False),
|
||||
tc(-123456789, TiffTags.SIGNED_LONG, False),
|
||||
tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
|
||||
tc(4.25, TiffTags.FLOAT, True),
|
||||
tc(4.25, TiffTags.DOUBLE, True),
|
||||
tc("custom tag value", TiffTags.ASCII, True),
|
||||
tc(b"custom tag value", TiffTags.BYTE, True),
|
||||
tc((4, 5, 6), TiffTags.SHORT, True),
|
||||
tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
|
||||
tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
|
||||
tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
|
||||
tc(
|
||||
Tc(4, TiffTags.SHORT, True),
|
||||
Tc(123456789, TiffTags.LONG, True),
|
||||
Tc(-4, TiffTags.SIGNED_BYTE, False),
|
||||
Tc(-4, TiffTags.SIGNED_SHORT, False),
|
||||
Tc(-123456789, TiffTags.SIGNED_LONG, False),
|
||||
Tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
|
||||
Tc(4.25, TiffTags.FLOAT, True),
|
||||
Tc(4.25, TiffTags.DOUBLE, True),
|
||||
Tc("custom tag value", TiffTags.ASCII, True),
|
||||
Tc(b"custom tag value", TiffTags.BYTE, True),
|
||||
Tc((4, 5, 6), TiffTags.SHORT, True),
|
||||
Tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
|
||||
Tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
|
||||
Tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
|
||||
Tc(
|
||||
(-123456789, 9, 34, 234, 219387, -92432323),
|
||||
TiffTags.SIGNED_LONG,
|
||||
False,
|
||||
),
|
||||
tc((4.25, 5.25), TiffTags.FLOAT, True),
|
||||
tc((4.25, 5.25), TiffTags.DOUBLE, True),
|
||||
Tc((4.25, 5.25), TiffTags.FLOAT, True),
|
||||
Tc((4.25, 5.25), TiffTags.DOUBLE, True),
|
||||
# array of TIFF_BYTE requires bytes instead of tuple for backwards
|
||||
# compatibility
|
||||
tc(bytes([4]), TiffTags.BYTE, True),
|
||||
tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
|
||||
Tc(bytes([4]), TiffTags.BYTE, True),
|
||||
Tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
libtiffs = [False]
|
||||
if Image.core.libtiff_support_custom_tags:
|
||||
libtiffs.append(True)
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
) -> None:
|
||||
im = hopper()
|
||||
|
||||
for libtiff in libtiffs:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
out = str(tmp_path / "temp.tif")
|
||||
im.save(out, tiffinfo=tiffinfo)
|
||||
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
) -> None:
|
||||
im = hopper()
|
||||
with Image.open(out) as reloaded:
|
||||
for tag, value in tiffinfo.items():
|
||||
reloaded_value = reloaded.tag_v2[tag]
|
||||
if (
|
||||
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
|
||||
and libtiff
|
||||
):
|
||||
# libtiff does not support real RATIONALS
|
||||
assert round(abs(float(reloaded_value) - float(value)), 7) == 0
|
||||
continue
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
im.save(out, tiffinfo=tiffinfo)
|
||||
assert reloaded_value == value
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
for tag, value in tiffinfo.items():
|
||||
reloaded_value = reloaded.tag_v2[tag]
|
||||
if (
|
||||
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
|
||||
and libtiff
|
||||
):
|
||||
# libtiff does not support real RATIONALS
|
||||
assert (
|
||||
round(abs(float(reloaded_value) - float(value)), 7) == 0
|
||||
)
|
||||
continue
|
||||
# Test with types
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
for tag, tagdata in custom.items():
|
||||
ifd[tag] = tagdata.value
|
||||
ifd.tagtype[tag] = tagdata.type
|
||||
check_tags(ifd)
|
||||
|
||||
assert reloaded_value == value
|
||||
# Test without types. This only works for some types, int for example are
|
||||
# always encoded as LONG and not SIGNED_LONG.
|
||||
check_tags(
|
||||
{
|
||||
tag: tagdata.value
|
||||
for tag, tagdata in custom.items()
|
||||
if tagdata.supported_by_default
|
||||
}
|
||||
)
|
||||
|
||||
# Test with types
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
for tag, tagdata in custom.items():
|
||||
ifd[tag] = tagdata.value
|
||||
ifd.tagtype[tag] = tagdata.type
|
||||
check_tags(ifd)
|
||||
|
||||
# Test without types. This only works for some types, int for example are
|
||||
# always encoded as LONG and not SIGNED_LONG.
|
||||
check_tags(
|
||||
{
|
||||
tag: tagdata.value
|
||||
for tag, tagdata in custom.items()
|
||||
if tagdata.supported_by_default
|
||||
}
|
||||
)
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
def test_osubfiletype(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||
im.tag_v2[OSUBFILETYPE] = 1
|
||||
im.save(outfile)
|
||||
|
||||
def test_subifd(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
@ -333,24 +364,24 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Should not segfault
|
||||
im.save(outfile)
|
||||
|
||||
def test_xmlpacket_tag(self, tmp_path: Path) -> None:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
def test_xmlpacket_tag(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
if 700 in reloaded.tag_v2:
|
||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||
|
||||
def test_int_dpi(self, tmp_path: Path) -> None:
|
||||
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
# issue #1765
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out, dpi=(72, 72))
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.info["dpi"] == (72.0, 72.0)
|
||||
|
||||
|
@ -412,13 +443,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert "temp.tif" == reread.tag_v2[269]
|
||||
assert "temp.tif" == reread.tag[269][0]
|
||||
|
||||
def test_12bit_rawmode(self) -> None:
|
||||
def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Are we generating the same interpretation
|
||||
of the image as Imagemagick is?"""
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/12bit.cropped.tif") as im:
|
||||
im.load()
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False)
|
||||
# to make the target --
|
||||
# convert 12bit.cropped.tif -depth 16 tmp.tif
|
||||
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
|
||||
|
@ -504,12 +535,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, out)
|
||||
|
||||
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||
def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None:
|
||||
def test_palette_save(
|
||||
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out)
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
# colormap/palette tag
|
||||
|
@ -524,7 +556,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, compression=compression)
|
||||
|
||||
def test_fp_leak(self) -> None:
|
||||
im = Image.open("Tests/images/hopper_g4_500.tif")
|
||||
im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif")
|
||||
assert im is not None
|
||||
fn = im.fp.fileno()
|
||||
|
||||
os.fstat(fn)
|
||||
|
@ -537,9 +570,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with pytest.raises(OSError):
|
||||
os.close(fn)
|
||||
|
||||
def test_multipage(self) -> None:
|
||||
def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# issue #862
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
||||
|
||||
|
@ -558,11 +591,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert im.size == (20, 20)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
|
||||
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_multipage_nframes(self) -> None:
|
||||
def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# issue #862
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
frames = im.n_frames
|
||||
assert frames == 3
|
||||
|
@ -571,10 +602,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Should not raise ValueError: I/O operation on closed file
|
||||
im.load()
|
||||
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_multipage_seek_backwards(self) -> None:
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
im.seek(1)
|
||||
im.load()
|
||||
|
@ -582,24 +611,21 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.seek(0)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
||||
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test__next(self) -> None:
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
assert not im.tag.next
|
||||
im.load()
|
||||
assert not im.tag.next
|
||||
|
||||
def test_4bit(self) -> None:
|
||||
def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
test_file = "Tests/images/hopper_gray_4bpp.tif"
|
||||
original = hopper("L")
|
||||
|
||||
# Act
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open(test_file) as im:
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
# Assert
|
||||
assert im.size == (128, 128)
|
||||
|
@ -639,12 +665,12 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert im2.mode == "L"
|
||||
assert_image_equal(im, im2)
|
||||
|
||||
def test_save_bytesio(self) -> None:
|
||||
def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# PR 1011
|
||||
# Test TIFF saving to io.BytesIO() object.
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
|
||||
# Generate test image
|
||||
pilim = hopper()
|
||||
|
@ -654,16 +680,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
pilim.save(buffer_io, format="tiff", compression=compression)
|
||||
buffer_io.seek(0)
|
||||
|
||||
assert_image_similar_tofile(pilim, buffer_io, 0)
|
||||
with Image.open(buffer_io) as saved_im:
|
||||
assert_image_similar(pilim, saved_im, 0)
|
||||
|
||||
save_bytesio()
|
||||
save_bytesio("raw")
|
||||
save_bytesio("packbits")
|
||||
save_bytesio("tiff_lzw")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_save_ycbcr(self, tmp_path: Path) -> None:
|
||||
im = hopper("YCbCr")
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
@ -673,25 +697,31 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert reloaded.tag_v2[530] == (1, 1)
|
||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||
|
||||
def test_exif_ifd(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
def test_exif_ifd(self) -> None:
|
||||
out = io.BytesIO()
|
||||
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
||||
assert im.tag_v2[34665] == 125456
|
||||
im.save(outfile)
|
||||
im.save(out, "TIFF")
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
with Image.open(out) as reloaded:
|
||||
assert 34665 not in reloaded.tag_v2
|
||||
|
||||
im.save(out, "TIFF", tiffinfo={34665: 125456})
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
if Image.core.libtiff_support_custom_tags:
|
||||
assert reloaded.tag_v2[34665] == 125456
|
||||
|
||||
def test_crashing_metadata(self, tmp_path: Path) -> None:
|
||||
def test_crashing_metadata(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
# issue 1597
|
||||
with Image.open("Tests/images/rdf.tif") as im:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
# this shouldn't crash
|
||||
im.save(out, format="TIFF")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
def test_page_number_x_0(self, tmp_path: Path) -> None:
|
||||
# Issue 973
|
||||
|
@ -716,41 +746,47 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
f.write(src.read())
|
||||
|
||||
im = Image.open(tmpfile)
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.n_frames
|
||||
im.close()
|
||||
# Should not raise PermissionError.
|
||||
os.remove(tmpfile)
|
||||
|
||||
def test_read_icc(self) -> None:
|
||||
def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc = img.info.get("icc_profile")
|
||||
assert icc is not None
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc_libtiff = img.info.get("icc_profile")
|
||||
assert icc_libtiff is not None
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
assert icc == icc_libtiff
|
||||
|
||||
def test_write_icc(self, tmp_path: Path) -> None:
|
||||
def check_write(libtiff: bool) -> None:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
@pytest.mark.parametrize(
|
||||
"libtiff",
|
||||
(
|
||||
pytest.param(
|
||||
True,
|
||||
marks=pytest.mark.skipif(
|
||||
not getattr(Image.core, "libtiff_support_custom_tags", False),
|
||||
reason="Custom tags not supported by older libtiff",
|
||||
),
|
||||
),
|
||||
False,
|
||||
),
|
||||
)
|
||||
def test_write_icc(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc_profile = img.info["icc_profile"]
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc_profile = img.info["icc_profile"]
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
img.save(out, icc_profile=icc_profile)
|
||||
with Image.open(out) as reloaded:
|
||||
assert icc_profile == reloaded.info["icc_profile"]
|
||||
|
||||
libtiffs = []
|
||||
if Image.core.libtiff_support_custom_tags:
|
||||
libtiffs.append(True)
|
||||
libtiffs.append(False)
|
||||
|
||||
for libtiff in libtiffs:
|
||||
check_write(libtiff)
|
||||
out = str(tmp_path / "temp.tif")
|
||||
img.save(out, icc_profile=icc_profile)
|
||||
with Image.open(out) as reloaded:
|
||||
assert icc_profile == reloaded.info["icc_profile"]
|
||||
|
||||
def test_multipage_compression(self) -> None:
|
||||
with Image.open("Tests/images/compression.tif") as im:
|
||||
|
@ -828,12 +864,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
|
||||
|
||||
def test_sampleformat_write(self, tmp_path: Path) -> None:
|
||||
def test_sampleformat_write(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
im = Image.new("F", (1, 1))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out)
|
||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.mode == "F"
|
||||
|
@ -1023,7 +1060,11 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
],
|
||||
)
|
||||
def test_wrong_bits_per_sample(
|
||||
self, file_name: str, mode: str, size: tuple[int, int], tile
|
||||
self,
|
||||
file_name: str,
|
||||
mode: str,
|
||||
size: tuple[int, int],
|
||||
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
|
||||
) -> None:
|
||||
with Image.open("Tests/images/" + file_name) as im:
|
||||
assert im.mode == mode
|
||||
|
@ -1079,15 +1120,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open(out) as im:
|
||||
im.load()
|
||||
|
||||
def test_realloc_overflow(self) -> None:
|
||||
TiffImagePlugin.READ_LIBTIFF = True
|
||||
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
|
||||
with pytest.raises(OSError) as e:
|
||||
im.load()
|
||||
|
||||
# Assert that the error code is IMAGING_CODEC_MEMORY
|
||||
assert str(e.value) == "-9"
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||
|
@ -1097,6 +1137,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
with Image.open(out) as im:
|
||||
# Assert that there are multiple strips
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
||||
|
||||
@pytest.mark.parametrize("argument", (True, False))
|
||||
|
@ -1110,9 +1151,10 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
|
||||
if argument:
|
||||
arguments["strip_size"] = 2**18
|
||||
im.save(out, **arguments)
|
||||
im.save(out, "TIFF", **arguments)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
||||
finally:
|
||||
TiffImagePlugin.STRIP_SIZE = 65536
|
||||
|
|
|
@ -19,7 +19,7 @@ def test_valid_file() -> None:
|
|||
# https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8
|
||||
# https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/
|
||||
test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara"
|
||||
saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png"
|
||||
saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff"
|
||||
|
||||
# Act
|
||||
with Image.open(test_file) as im:
|
||||
|
|
39
Tests/test_file_mpeg.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, MpegImagePlugin
|
||||
|
||||
|
||||
def test_identify() -> None:
|
||||
# Arrange
|
||||
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
|
||||
|
||||
# Act
|
||||
with Image.open(b) as im:
|
||||
# Assert
|
||||
assert im.format == "MPEG"
|
||||
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (16, 1)
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
# Arrange
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(SyntaxError):
|
||||
MpegImagePlugin.MpegImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_load() -> None:
|
||||
# Arrange
|
||||
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
|
||||
|
||||
with Image.open(b) as im:
|
||||
# Act / Assert: cannot load
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
|
@ -6,7 +6,7 @@ from typing import Any
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, ImageFile, MpoImagePlugin
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -20,14 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
|||
pytestmark = skip_unless_feature("jpg")
|
||||
|
||||
|
||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
|
||||
out = BytesIO()
|
||||
im.save(out, "MPO", **options)
|
||||
test_bytes = out.tell()
|
||||
out.seek(0)
|
||||
im = Image.open(out)
|
||||
im.bytes = test_bytes # for testing only
|
||||
return im
|
||||
return Image.open(out)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
|
@ -88,7 +85,9 @@ def test_exif(test_file: str) -> None:
|
|||
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
|
||||
|
||||
for im in (im_original, im_reloaded):
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
info = im._getexif()
|
||||
assert info is not None
|
||||
assert info[272] == "Nintendo 3DS"
|
||||
assert info[296] == 2
|
||||
assert info[34665] == 188
|
||||
|
@ -96,7 +95,7 @@ def test_exif(test_file: str) -> None:
|
|||
|
||||
def test_frame_size() -> None:
|
||||
# This image has been hexedited to contain a different size
|
||||
# in the EXIF data of the second frame
|
||||
# in the SOF marker of the second frame
|
||||
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
|
||||
assert im.size == (640, 480)
|
||||
|
||||
|
@ -229,6 +228,17 @@ def test_eoferror() -> None:
|
|||
im.seek(n_frames - 1)
|
||||
|
||||
|
||||
def test_adopt_jpeg() -> None:
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
with pytest.raises(ValueError):
|
||||
MpoImagePlugin.MpoImageFile.adopt(im)
|
||||
|
||||
|
||||
def test_ultra_hdr() -> None:
|
||||
with Image.open("Tests/images/ultrahdr.jpg") as im:
|
||||
assert im.format == "JPEG"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_image_grab(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -273,6 +283,8 @@ def test_save_all() -> None:
|
|||
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
|
||||
|
||||
assert_image_equal(im, im_reloaded)
|
||||
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
|
||||
assert im_reloaded.mpinfo is not None
|
||||
assert im_reloaded.mpinfo[45056] == b"0100"
|
||||
|
||||
im_reloaded.seek(1)
|
||||
|
|
|
@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
|
|||
assert isinstance(im, MspImagePlugin.MspImageFile)
|
||||
|
||||
|
||||
def _assert_file_image_equal(source_path, target_path) -> None:
|
||||
def _assert_file_image_equal(source_path: str, target_path: str) -> None:
|
||||
with Image.open(source_path) as im:
|
||||
assert_image_equal_tofile(im, target_path)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin
|
|||
from .helper import assert_image_equal, hopper
|
||||
|
||||
|
||||
def _roundtrip(tmp_path: Path, im) -> None:
|
||||
def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
|
||||
f = str(tmp_path / "temp.pcx")
|
||||
im.save(f)
|
||||
with Image.open(f) as im2:
|
||||
|
@ -44,7 +44,7 @@ def test_invalid_file() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
||||
def test_odd(tmp_path: Path, mode) -> None:
|
||||
def test_odd(tmp_path: Path, mode: str) -> None:
|
||||
# See issue #523, odd sized images should have a stride that's even.
|
||||
# Not that ImageMagick or GIMP write PCX that way.
|
||||
# We were not handling properly.
|
||||
|
@ -76,6 +76,7 @@ def test_pil184() -> None:
|
|||
def test_1px_width(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (1, 256))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for y in range(256):
|
||||
px[0, y] = y
|
||||
_roundtrip(tmp_path, im)
|
||||
|
@ -84,12 +85,13 @@ def test_1px_width(tmp_path: Path) -> None:
|
|||
def test_large_count(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (256, 1))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for x in range(256):
|
||||
px[x, 0] = x // 67 * 67
|
||||
_roundtrip(tmp_path, im)
|
||||
|
||||
|
||||
def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
|
||||
def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None:
|
||||
_last = ImageFile.MAXBLOCK
|
||||
ImageFile.MAXBLOCK = size
|
||||
try:
|
||||
|
@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None:
|
|||
def test_break_in_count_overflow(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (256, 5))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for y in range(4):
|
||||
for x in range(256):
|
||||
px[x, y] = x % 128
|
||||
|
@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
|
|||
def test_break_one_in_loop(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (256, 5))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for y in range(5):
|
||||
for x in range(256):
|
||||
px[x, y] = x % 128
|
||||
|
@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
|
|||
def test_break_many_in_loop(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (256, 5))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for y in range(4):
|
||||
for x in range(256):
|
||||
px[x, y] = x % 128
|
||||
|
@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
|
|||
def test_break_one_at_end(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (256, 5))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for y in range(5):
|
||||
for x in range(256):
|
||||
px[x, y] = x % 128
|
||||
|
@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
|
|||
def test_break_many_at_end(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (256, 5))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for y in range(5):
|
||||
for x in range(256):
|
||||
px[x, y] = x % 128
|
||||
|
@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
|
|||
def test_break_padding(tmp_path: Path) -> None:
|
||||
im = Image.new("L", (257, 5))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
for y in range(5):
|
||||
for x in range(257):
|
||||
px[x, y] = x % 128
|
||||
|
|
|
@ -5,7 +5,9 @@ import os
|
|||
import os.path
|
||||
import tempfile
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -14,7 +16,7 @@ from PIL import Image, PdfParser, features
|
|||
from .helper import hopper, mark_if_feature_version, skip_unless_feature
|
||||
|
||||
|
||||
def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
||||
def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str:
|
||||
# Arrange
|
||||
im = hopper(mode)
|
||||
outfile = str(tmp_path / ("temp_" + mode + ".pdf"))
|
||||
|
@ -41,13 +43,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs):
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
|
||||
def test_save(tmp_path: Path, mode) -> None:
|
||||
def test_save(tmp_path: Path, mode: str) -> None:
|
||||
helper_save_as_pdf(tmp_path, mode)
|
||||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
||||
def test_save_alpha(tmp_path: Path, mode) -> None:
|
||||
def test_save_alpha(tmp_path: Path, mode: str) -> None:
|
||||
helper_save_as_pdf(tmp_path, mode)
|
||||
|
||||
|
||||
|
@ -112,11 +114,11 @@ def test_resolution(tmp_path: Path) -> None:
|
|||
{"dpi": (75, 150), "resolution": 200},
|
||||
),
|
||||
)
|
||||
def test_dpi(params, tmp_path: Path) -> None:
|
||||
def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
|
||||
outfile = str(tmp_path / "temp.pdf")
|
||||
im.save(outfile, **params)
|
||||
im.save(outfile, "PDF", **params)
|
||||
|
||||
with open(outfile, "rb") as fp:
|
||||
contents = fp.read()
|
||||
|
@ -156,7 +158,7 @@ def test_save_all(tmp_path: Path) -> None:
|
|||
assert os.path.getsize(outfile) > 0
|
||||
|
||||
# Test appending using a generator
|
||||
def im_generator(ims):
|
||||
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
||||
|
@ -226,7 +228,8 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
|
|||
im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True)
|
||||
|
||||
|
||||
def check_pdf_pages_consistency(pdf) -> None:
|
||||
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
|
||||
assert pdf.pages_ref is not None
|
||||
pages_info = pdf.read_indirect(pdf.pages_ref)
|
||||
assert b"Parent" not in pages_info
|
||||
assert b"Kids" in pages_info
|
||||
|
@ -339,7 +342,7 @@ def test_pdf_append_to_bytesio() -> None:
|
|||
@pytest.mark.timeout(1)
|
||||
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
|
||||
@pytest.mark.parametrize("newline", (b"\r", b"\n"))
|
||||
def test_redos(newline) -> None:
|
||||
def test_redos(newline: bytes) -> None:
|
||||
malicious = b" trailer<<>>" + newline * 3456
|
||||
|
||||
# This particular exception isn't relevant here.
|
||||
|
|
|
@ -6,7 +6,8 @@ import warnings
|
|||
import zlib
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from types import ModuleType
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -23,6 +24,7 @@ from .helper import (
|
|||
skip_unless_feature,
|
||||
)
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
|
@ -39,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC
|
|||
|
||||
def chunk(cid: bytes, *data: bytes) -> bytes:
|
||||
test_file = BytesIO()
|
||||
PngImagePlugin.putchunk(*(test_file, cid) + data)
|
||||
PngImagePlugin.putchunk(test_file, cid, *data)
|
||||
return test_file.getvalue()
|
||||
|
||||
|
||||
|
@ -57,11 +59,11 @@ def load(data: bytes) -> Image.Image:
|
|||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
|
||||
out = BytesIO()
|
||||
im.save(out, "PNG", **options)
|
||||
out.seek(0)
|
||||
return Image.open(out)
|
||||
return cast(PngImagePlugin.PngImageFile, Image.open(out))
|
||||
|
||||
|
||||
@skip_unless_feature("zlib")
|
||||
|
@ -83,7 +85,9 @@ class TestFilePng:
|
|||
|
||||
def test_sanity(self, tmp_path: Path) -> None:
|
||||
# internal version number
|
||||
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
|
||||
version = features.version_codec("zlib")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)
|
||||
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
|
@ -100,7 +104,7 @@ class TestFilePng:
|
|||
im = hopper(mode)
|
||||
im.save(test_file)
|
||||
with Image.open(test_file) as reloaded:
|
||||
if mode in ("I;16", "I;16B"):
|
||||
if mode in ("I", "I;16B"):
|
||||
reloaded = reloaded.convert(mode)
|
||||
assert_image_equal(reloaded, im)
|
||||
|
||||
|
@ -302,8 +306,8 @@ class TestFilePng:
|
|||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||
|
||||
def test_save_grayscale_transparency(self, tmp_path: Path) -> None:
|
||||
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
|
||||
in_file = "Tests/images/" + mode.lower() + "_trns.png"
|
||||
for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items():
|
||||
in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png"
|
||||
with Image.open(in_file) as im:
|
||||
assert im.mode == mode
|
||||
assert im.info["transparency"] == 255
|
||||
|
@ -531,8 +535,10 @@ class TestFilePng:
|
|||
|
||||
def test_repr_png(self) -> None:
|
||||
im = hopper()
|
||||
b = im._repr_png_()
|
||||
assert b is not None
|
||||
|
||||
with Image.open(BytesIO(im._repr_png_())) as repr_png:
|
||||
with Image.open(BytesIO(b)) as repr_png:
|
||||
assert repr_png.format == "PNG"
|
||||
assert_image_equal(im, repr_png)
|
||||
|
||||
|
@ -617,6 +623,10 @@ class TestFilePng:
|
|||
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
||||
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
||||
|
||||
def test_unknown_compression_method(self) -> None:
|
||||
with pytest.raises(SyntaxError, match="Unknown compression method"):
|
||||
PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
|
||||
|
||||
def test_padded_idat(self) -> None:
|
||||
# This image has been manually hexedited
|
||||
# so that the IDAT chunk has padding at the end
|
||||
|
@ -647,11 +657,12 @@ class TestFilePng:
|
|||
png.call(cid, 0, 0)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
def test_specify_bits(self, tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("save_all", (True, False))
|
||||
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
|
||||
im = hopper("P")
|
||||
|
||||
out = str(tmp_path / "temp.png")
|
||||
im.save(out, bits=4)
|
||||
im.save(out, bits=4, save_all=save_all)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert len(reloaded.png.im_palette[1]) == 48
|
||||
|
@ -675,6 +686,7 @@ class TestFilePng:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
@ -759,14 +771,10 @@ class TestFilePng:
|
|||
def test_save_stdout(self, buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
|
||||
|
@ -776,11 +784,23 @@ class TestFilePng:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
||||
|
||||
def test_truncated_end_chunk(self) -> None:
|
||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
|
||||
@skip_unless_feature("zlib")
|
||||
|
|
|
@ -88,7 +88,7 @@ def test_16bit_pgm() -> None:
|
|||
assert im.size == (20, 100)
|
||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png")
|
||||
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff")
|
||||
|
||||
|
||||
def test_16bit_pgm_write(tmp_path: Path) -> None:
|
||||
|
@ -241,13 +241,23 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
|
|||
im.load()
|
||||
|
||||
|
||||
def test_plain_ppm_value_negative(tmp_path: Path) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P3\n128 128\n255\n-1")
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError, match="Channel value is negative"):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_plain_ppm_value_too_large(tmp_path: Path) -> None:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P3\n128 128\n255\n256")
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Channel value too large"):
|
||||
im.load()
|
||||
|
||||
|
||||
|
@ -358,14 +368,10 @@ def test_mimetypes(tmp_path: Path) -> None:
|
|||
def test_save_stdout(buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
mystdout = MyStdOut()
|
||||
else:
|
||||
mystdout = BytesIO()
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
|
||||
|
@ -375,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
|
|||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if buffer:
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_FILE)
|
||||
|
|
|
@ -113,6 +113,11 @@ def test_rgba() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
|
||||
|
||||
|
||||
def test_negative_top_left_layer() -> None:
|
||||
with Image.open("Tests/images/negative_top_left_layer.psd") as im:
|
||||
assert im.layers[0][2] == (-50, -50, 50, 50)
|
||||
|
||||
|
||||
def test_layer_skip() -> None:
|
||||
with Image.open("Tests/images/five_channels.psd") as im:
|
||||
assert im.n_frames == 1
|
||||
|
@ -145,20 +150,26 @@ def test_combined_larger_than_size() -> None:
|
|||
@pytest.mark.parametrize(
|
||||
"test_file,raises",
|
||||
[
|
||||
(
|
||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||
Image.UnidentifiedImageError,
|
||||
),
|
||||
(
|
||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||
Image.UnidentifiedImageError,
|
||||
),
|
||||
("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
|
||||
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
|
||||
],
|
||||
)
|
||||
def test_crashes(test_file, raises) -> None:
|
||||
def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with pytest.raises(raises):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
[
|
||||
"Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
|
||||
"Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
|
||||
],
|
||||
)
|
||||
def test_layer_crashes(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
with pytest.raises(SyntaxError):
|
||||
im.layers
|
||||
|
|
|
@ -9,7 +9,7 @@ import pytest
|
|||
|
||||
from PIL import Image, ImageSequence, SpiderImagePlugin
|
||||
|
||||
from .helper import assert_image_equal_tofile, hopper, is_pypy
|
||||
from .helper import assert_image_equal, hopper, is_pypy
|
||||
|
||||
TEST_FILE = "Tests/images/hopper.spider"
|
||||
|
||||
|
@ -105,6 +105,7 @@ def test_load_image_series() -> None:
|
|||
img_list = SpiderImagePlugin.loadImageSeries(file_list)
|
||||
|
||||
# Assert
|
||||
assert img_list is not None
|
||||
assert len(img_list) == 1
|
||||
assert isinstance(img_list[0], Image.Image)
|
||||
assert img_list[0].size == (128, 128)
|
||||
|
@ -160,4 +161,5 @@ def test_odd_size() -> None:
|
|||
im.save(data, format="SPIDER")
|
||||
|
||||
data.seek(0)
|
||||
assert_image_equal_tofile(im, data)
|
||||
with Image.open(data) as im2:
|
||||
assert_image_equal(im, im2)
|
||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", _MODES)
|
||||
def test_sanity(mode, tmp_path: Path) -> None:
|
||||
def roundtrip(original_im) -> None:
|
||||
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||
def roundtrip(original_im: Image.Image) -> None:
|
||||
out = str(tmp_path / "temp.tga")
|
||||
|
||||
original_im.save(out, rle=rle)
|
||||
|
@ -65,14 +65,28 @@ def test_sanity(mode, tmp_path: Path) -> None:
|
|||
roundtrip(original_im)
|
||||
|
||||
|
||||
def test_palette_depth_8(tmp_path: Path) -> None:
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
Image.open("Tests/images/p_8.tga")
|
||||
|
||||
|
||||
def test_palette_depth_16(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/p_16.tga") as im:
|
||||
assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
|
||||
assert im.palette.mode == "RGBA"
|
||||
assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png")
|
||||
|
||||
out = str(tmp_path / "temp.png")
|
||||
im.save(out)
|
||||
with Image.open(out) as reloaded:
|
||||
assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
|
||||
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
|
||||
|
||||
|
||||
def test_rgba_16() -> None:
|
||||
with Image.open("Tests/images/rgba16.tga") as im:
|
||||
assert im.mode == "RGBA"
|
||||
|
||||
assert im.getpixel((0, 0)) == (172, 0, 255, 255)
|
||||
assert im.getpixel((1, 0)) == (0, 255, 82, 0)
|
||||
|
||||
|
||||
def test_id_field() -> None:
|
||||
|
@ -133,6 +147,11 @@ def test_small_palette(tmp_path: Path) -> None:
|
|||
assert reloaded.getpalette() == colors
|
||||
|
||||
|
||||
def test_missing_palette() -> None:
|
||||
with Image.open("Tests/images/dilation4.lut") as im:
|
||||
assert im.mode == "L"
|
||||
|
||||
|
||||
def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||
im = hopper("PA")
|
||||
out = str(tmp_path / "temp.tga")
|
||||
|
|
|
@ -2,8 +2,10 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import warnings
|
||||
from collections.abc import Generator
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -20,6 +22,7 @@ from .helper import (
|
|||
is_win32,
|
||||
)
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
|
@ -75,6 +78,7 @@ class TestFileTiff:
|
|||
|
||||
def test_seek_after_close(self) -> None:
|
||||
im = Image.open("Tests/images/multipage.tiff")
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -110,10 +114,14 @@ class TestFileTiff:
|
|||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||
|
||||
def test_seek_too_large(self) -> None:
|
||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||
Image.open("Tests/images/seek_too_large.tif")
|
||||
|
||||
def test_set_legacy_api(self) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
with pytest.raises(Exception) as e:
|
||||
ifd.legacy_api = None
|
||||
ifd.legacy_api = False
|
||||
assert str(e.value) == "Not allowing setting of legacy api"
|
||||
|
||||
def test_xyres_tiff(self) -> None:
|
||||
|
@ -156,7 +164,7 @@ class TestFileTiff:
|
|||
"resolution_unit, dpi",
|
||||
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
||||
)
|
||||
def test_load_float_dpi(self, resolution_unit, dpi) -> None:
|
||||
def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None:
|
||||
with Image.open(
|
||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||
) as im:
|
||||
|
@ -284,7 +292,7 @@ class TestFileTiff:
|
|||
("Tests/images/multipage.tiff", 3),
|
||||
),
|
||||
)
|
||||
def test_n_frames(self, path, n_frames) -> None:
|
||||
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
@ -402,7 +410,7 @@ class TestFileTiff:
|
|||
assert len_before == len_after + 1
|
||||
|
||||
@pytest.mark.parametrize("legacy_api", (False, True))
|
||||
def test_load_byte(self, legacy_api) -> None:
|
||||
def test_load_byte(self, legacy_api: bool) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
data = b"abc"
|
||||
ret = ifd.load_byte(data, legacy_api)
|
||||
|
@ -417,13 +425,13 @@ class TestFileTiff:
|
|||
def test_load_float(self) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
data = b"abcdabcd"
|
||||
ret = ifd.load_float(data, False)
|
||||
ret = getattr(ifd, "load_float")(data, False)
|
||||
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
|
||||
|
||||
def test_load_double(self) -> None:
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
data = b"abcdefghabcdefgh"
|
||||
ret = ifd.load_double(data, False)
|
||||
ret = getattr(ifd, "load_double")(data, False)
|
||||
assert ret == (8.540883223036124e194, 8.540883223036124e194)
|
||||
|
||||
def test_ifd_tag_type(self) -> None:
|
||||
|
@ -431,7 +439,7 @@ class TestFileTiff:
|
|||
assert 0x8825 in im.tag_v2
|
||||
|
||||
def test_exif(self, tmp_path: Path) -> None:
|
||||
def check_exif(exif) -> None:
|
||||
def check_exif(exif: Image.Exif) -> None:
|
||||
assert sorted(exif.keys()) == [
|
||||
256,
|
||||
257,
|
||||
|
@ -511,7 +519,7 @@ class TestFileTiff:
|
|||
assert im.getexif()[273] == (1408, 1907)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||
def test_photometric(self, mode, tmp_path: Path) -> None:
|
||||
def test_photometric(self, mode: str, tmp_path: Path) -> None:
|
||||
filename = str(tmp_path / "temp.tif")
|
||||
im = hopper(mode)
|
||||
im.save(filename, tiffinfo={262: 0})
|
||||
|
@ -592,7 +600,7 @@ class TestFileTiff:
|
|||
def test_with_underscores(self, tmp_path: Path) -> None:
|
||||
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
|
||||
filename = str(tmp_path / "temp.tif")
|
||||
hopper("RGB").save(filename, **kwargs)
|
||||
hopper("RGB").save(filename, "TIFF", **kwargs)
|
||||
with Image.open(filename) as im:
|
||||
# legacy interface
|
||||
assert im.tag[X_RESOLUTION][0][0] == 72
|
||||
|
@ -614,12 +622,29 @@ class TestFileTiff:
|
|||
|
||||
assert_image_equal_tofile(im, tmpfile)
|
||||
|
||||
def test_iptc(self, tmp_path: Path) -> None:
|
||||
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
im.load()
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
ifd[33723] = 1
|
||||
ifd.tagtype[33723] = 4
|
||||
im.tag_v2 = ifd
|
||||
im.save(outfile)
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert 33723 not in im.tag_v2
|
||||
|
||||
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
im.save(outfile, tiffinfo={278: 256})
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2[278] == 256
|
||||
|
||||
def test_strip_raw(self) -> None:
|
||||
|
@ -660,7 +685,7 @@ class TestFileTiff:
|
|||
assert_image_equal_tofile(reloaded, infile)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
def test_palette(self, mode, tmp_path: Path) -> None:
|
||||
def test_palette(self, mode: str, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
||||
im = hopper(mode)
|
||||
|
@ -689,7 +714,7 @@ class TestFileTiff:
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Test appending using a generator
|
||||
def im_generator(ims):
|
||||
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
mp = BytesIO()
|
||||
|
@ -751,6 +776,7 @@ class TestFileTiff:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
xmp = im.getxmp()
|
||||
|
||||
description = xmp["xmpmeta"]["RDF"]["Description"]
|
||||
|
@ -860,7 +886,7 @@ class TestFileTiff:
|
|||
],
|
||||
)
|
||||
@pytest.mark.timeout(2)
|
||||
def test_oom(self, test_file) -> None:
|
||||
def test_oom(self, test_file: str) -> None:
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open(test_file):
|
||||
|
|
|
@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
|
|||
|
||||
from .helper import assert_deep_equal, hopper
|
||||
|
||||
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
|
||||
TAG_IDS: dict[str, int] = {
|
||||
info.name: info.value
|
||||
for info in TiffTags.TAGS_V2.values()
|
||||
if info.value is not None
|
||||
}
|
||||
|
||||
|
||||
def test_rt_metadata(tmp_path: Path) -> None:
|
||||
|
@ -189,7 +193,9 @@ def test_iptc(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
|
||||
def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
|
||||
def test_writing_other_types_to_ascii(
|
||||
value: bytes | int, expected: str, tmp_path: Path
|
||||
) -> None:
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
tag = TiffTags.TAGS_V2[271]
|
||||
|
@ -206,7 +212,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
||||
def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
|
||||
def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
|
@ -222,14 +228,17 @@ def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None:
|
|||
assert reloaded.tag_v2[700] == b"\x01"
|
||||
|
||||
|
||||
def test_writing_other_types_to_undefined(tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
||||
def test_writing_other_types_to_undefined(
|
||||
value: int | IFDRational, tmp_path: Path
|
||||
) -> None:
|
||||
im = hopper()
|
||||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
tag = TiffTags.TAGS_V2[33723]
|
||||
assert tag.type == TiffTags.UNDEFINED
|
||||
|
||||
info[33723] = 1
|
||||
info[33723] = value
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
im.save(out, tiffinfo=info)
|
||||
|
@ -406,8 +415,8 @@ def test_empty_values() -> None:
|
|||
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
||||
info.load(data)
|
||||
# Should not raise ValueError.
|
||||
info = dict(info)
|
||||
assert 33432 in info
|
||||
info_dict = dict(info)
|
||||
assert 33432 in info_dict
|
||||
|
||||
|
||||
def test_photoshop_info(tmp_path: Path) -> None:
|
||||
|
|
|
@ -5,6 +5,7 @@ import re
|
|||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -49,7 +50,9 @@ class TestFileWebp:
|
|||
def test_version(self) -> None:
|
||||
_webp.WebPDecoderVersion()
|
||||
_webp.WebPDecoderBuggyAlpha()
|
||||
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp"))
|
||||
version = features.version_module("webp")
|
||||
assert version is not None
|
||||
assert re.search(r"\d+\.\d+\.\d+$", version)
|
||||
|
||||
def test_read_rgb(self) -> None:
|
||||
"""
|
||||
|
@ -68,7 +71,9 @@ class TestFileWebp:
|
|||
# dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm
|
||||
assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0)
|
||||
|
||||
def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None:
|
||||
def _roundtrip(
|
||||
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
|
||||
) -> None:
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
|
||||
hopper(mode).save(temp_file, **args)
|
||||
|
@ -196,7 +201,9 @@ class TestFileWebp:
|
|||
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
|
||||
)
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_invalid_background(self, background, tmp_path: Path) -> None:
|
||||
def test_invalid_background(
|
||||
self, background: int | tuple[int, ...], tmp_path: Path
|
||||
) -> None:
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = hopper()
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -151,3 +151,15 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None:
|
|||
target = im.convert("RGBA")
|
||||
|
||||
assert_image_similar(image, target, 25.0)
|
||||
|
||||
|
||||
def test_alpha_quality(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
out = str(tmp_path / "temp.webp")
|
||||
im.save(out)
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
im.save(out_quality, alpha_quality=50)
|
||||
with Image.open(out) as reloaded:
|
||||
with Image.open(out_quality) as reloaded_quality:
|
||||
assert reloaded.tobytes() != reloaded_quality.tobytes()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
@ -52,8 +53,9 @@ def test_write_animation_L(tmp_path: Path) -> None:
|
|||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||
|
||||
if is_big_endian():
|
||||
webp = parse_version(features.version_module("webp"))
|
||||
if webp < parse_version("1.2.2"):
|
||||
version = features.version_module("webp")
|
||||
assert version is not None
|
||||
if parse_version(version) < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
orig.seek(orig.n_frames - 1)
|
||||
im.seek(im.n_frames - 1)
|
||||
|
@ -68,7 +70,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
|||
are visually similar to the originals.
|
||||
"""
|
||||
|
||||
def check(temp_file) -> None:
|
||||
def check(temp_file: str) -> None:
|
||||
with Image.open(temp_file) as im:
|
||||
assert im.n_frames == 2
|
||||
|
||||
|
@ -78,8 +80,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
|||
|
||||
# Compare second frame to original
|
||||
if is_big_endian():
|
||||
webp = parse_version(features.version_module("webp"))
|
||||
if webp < parse_version("1.2.2"):
|
||||
version = features.version_module("webp")
|
||||
assert version is not None
|
||||
if parse_version(version) < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
im.seek(1)
|
||||
im.load()
|
||||
|
@ -94,7 +97,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
|||
check(temp_file1)
|
||||
|
||||
# Tests appending using a generator
|
||||
def im_generator(ims):
|
||||
def im_generator(
|
||||
ims: list[Image.Image],
|
||||
) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
temp_file2 = str(tmp_path / "temp_generator.webp")
|
||||
|
@ -188,3 +193,21 @@ def test_seek_errors() -> None:
|
|||
|
||||
with pytest.raises(EOFError):
|
||||
im.seek(42)
|
||||
|
||||
|
||||
def test_alpha_quality(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
first_frame = Image.new("L", im.size)
|
||||
|
||||
out = str(tmp_path / "temp.webp")
|
||||
first_frame.save(out, save_all=True, append_images=[im])
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
first_frame.save(
|
||||
out_quality, save_all=True, append_images=[im], alpha_quality=50
|
||||
)
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded.seek(1)
|
||||
with Image.open(out_quality) as reloaded_quality:
|
||||
reloaded_quality.seek(1)
|
||||
assert reloaded.tobytes() != reloaded_quality.tobytes()
|
||||
|
|
|
@ -129,6 +129,7 @@ def test_getxmp() -> None:
|
|||
):
|
||||
assert im.getxmp() == {}
|
||||
else:
|
||||
assert "xmp" in im.info
|
||||
assert (
|
||||
im.getxmp()["xmpmeta"]["xmptk"]
|
||||
== "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
|
||||
|
|