Merge branch 'main' into convert_mode
|
@ -3,7 +3,7 @@
|
|||
# gather the coverage data
|
||||
python3 -m pip install codecov
|
||||
if [[ $MATRIX_DOCKER ]]; then
|
||||
coverage xml --ignore-errors
|
||||
python3 -m coverage xml --ignore-errors
|
||||
else
|
||||
coverage xml
|
||||
python3 -m coverage xml
|
||||
fi
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
coverage erase
|
||||
python3 -m coverage erase
|
||||
if [ $(uname) == "Darwin" ]; then
|
||||
export CPPFLAGS="-I/usr/local/miniconda/include";
|
||||
fi
|
||||
|
|
|
@ -13,13 +13,17 @@ aptget_update()
|
|||
return 1
|
||||
fi
|
||||
}
|
||||
aptget_update || aptget_update retry || aptget_update retry
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
aptget_update || aptget_update retry || aptget_update retry
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
|
||||
fi
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade wheel
|
||||
|
@ -32,24 +36,28 @@ python3 -m pip install -U pytest-cov
|
|||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
python3 -m pip install test-image-results
|
||||
python3 -m pip install numpy
|
||||
|
||||
# PyQt5 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
# arm64, ppc64le, s390x CPUs:
|
||||
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
|
||||
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
|
||||
python3 -m pip install pyqt5
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
# TODO Remove condition when NumPy supports 3.11
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0
|
||||
python3 -m pip install pyqt6
|
||||
fi
|
||||
|
||||
# webp
|
||||
pushd depends && ./install_webp.sh && popd
|
||||
|
||||
# libimagequant
|
||||
pushd depends && ./install_imagequant.sh && popd
|
||||
|
||||
# raqm
|
||||
pushd depends && ./install_raqm.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
else
|
||||
cd depends && ./install_extra_test_images.sh && cd ..
|
||||
fi
|
||||
|
||||
# webp
|
||||
pushd depends && ./install_webp.sh && popd
|
||||
|
||||
# libimagequant
|
||||
pushd depends && ./install_imagequant.sh && popd
|
||||
|
||||
# raqm
|
||||
pushd depends && ./install_raqm.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
|
@ -16,7 +16,6 @@ trim_trailing_whitespace = true
|
|||
[*.yml]
|
||||
# Two-space indentation
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
|
|
2
.github/CONTRIBUTING.md
vendored
|
@ -4,7 +4,7 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
|
|||
|
||||
## Bug fixes, feature additions, etc.
|
||||
|
||||
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
|
||||
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
|
||||
|
||||
- Fork the Pillow repository.
|
||||
- Create a branch from `main`.
|
||||
|
|
1
.github/mergify.yml
vendored
|
@ -8,6 +8,7 @@ pull_request_rules:
|
|||
- status-success=Docker Test Successful
|
||||
- status-success=Windows Test Successful
|
||||
- status-success=MinGW Test Successful
|
||||
- status-success=Cygwin Test Successful
|
||||
- status-success=continuous-integration/appveyor/pr
|
||||
actions:
|
||||
merge:
|
||||
|
|
3
.github/workflows/macos-install.sh
vendored
|
@ -15,7 +15,8 @@ python3 -m pip install pyroma
|
|||
python3 -m pip install test-image-results
|
||||
|
||||
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
|
||||
python3 -m pip install numpy
|
||||
# TODO Remove condition when NumPy supports 3.11
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
27
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Close stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "10 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository_owner == 'python-pillow'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: "Check issues"
|
||||
uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "Awaiting OP Action"
|
||||
close-issue-message: "Closing this issue as no feedback has been received."
|
||||
days-before-stale: 7
|
||||
days-before-issue-close: 0
|
||||
days-before-pr-close: -1
|
||||
labels-to-remove-when-unstale: "Awaiting OP Action"
|
107
.github/workflows/test-cygwin.yml
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
name: Test Cygwin
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-minor-version: [7, 8, 9]
|
||||
|
||||
timeout-minutes: 40
|
||||
|
||||
name: Python 3.${{ matrix.python-minor-version }}
|
||||
|
||||
steps:
|
||||
- name: Fix line endings
|
||||
run: |
|
||||
git config --global core.autocrlf input
|
||||
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: cygwin/cygwin-install-action@v2
|
||||
with:
|
||||
platform: x86_64
|
||||
packages: >
|
||||
ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel
|
||||
libimagequant-devel libjpeg-devel liblapack-devel
|
||||
liblcms2-devel libopenjp2-devel libraqm-devel
|
||||
libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0
|
||||
make netpbm perl
|
||||
python3${{ matrix.python-minor-version }}-cffi
|
||||
python3${{ matrix.python-minor-version }}-cython
|
||||
python3${{ matrix.python-minor-version }}-devel
|
||||
python3${{ matrix.python-minor-version }}-numpy
|
||||
python3${{ matrix.python-minor-version }}-sip
|
||||
python3${{ matrix.python-minor-version }}-tkinter
|
||||
qt5-devel-tools subversion xorg-server-extra zlib-devel
|
||||
|
||||
- name: Add Lapack to PATH
|
||||
uses: egor-tensin/cleanup-path@v1
|
||||
with:
|
||||
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
|
||||
|
||||
- name: Build system information
|
||||
run: |
|
||||
dash.exe -c "python3 .github/workflows/system-info.py"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
bash.exe .ci/install.sh
|
||||
|
||||
- name: Install a different NumPy
|
||||
shell: dash.exe -l "{0}"
|
||||
run: |
|
||||
python3 -m pip install -U 'numpy!=1.21.*'
|
||||
|
||||
- name: Build
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
run: |
|
||||
.ci/build.sh
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
|
||||
|
||||
- name: Prepare to upload errors
|
||||
if: failure()
|
||||
run: |
|
||||
dash.exe -c "mkdir -p Tests/errors"
|
||||
|
||||
- name: Upload errors
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: errors
|
||||
path: Tests/errors
|
||||
|
||||
- name: After success
|
||||
run: |
|
||||
bash.exe .ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Cygwin
|
||||
name: Cygwin Python 3.${{ matrix.python-minor-version }}
|
||||
|
||||
success:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Cygwin Test Successful
|
||||
steps:
|
||||
- name: Success
|
||||
run: echo Cygwin Test Successful
|
14
.github/workflows/test-docker.yml
vendored
|
@ -11,9 +11,9 @@ jobs:
|
|||
matrix:
|
||||
docker: [
|
||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||
ubuntu-20.04-focal-arm64v8,
|
||||
ubuntu-20.04-focal-ppc64le,
|
||||
ubuntu-20.04-focal-s390x,
|
||||
ubuntu-22.04-jammy-arm64v8,
|
||||
ubuntu-22.04-jammy-ppc64le,
|
||||
ubuntu-22.04-jammy-s390x,
|
||||
# Then run the remainder
|
||||
alpine,
|
||||
amazon-2-amd64,
|
||||
|
@ -24,17 +24,19 @@ jobs:
|
|||
debian-10-buster-x86,
|
||||
debian-11-bullseye-x86,
|
||||
fedora-35-amd64,
|
||||
fedora-36-amd64,
|
||||
gentoo,
|
||||
ubuntu-18.04-bionic-amd64,
|
||||
ubuntu-20.04-focal-amd64,
|
||||
ubuntu-22.04-jammy-amd64,
|
||||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-20.04-focal-arm64v8"
|
||||
- docker: "ubuntu-22.04-jammy-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
- docker: "ubuntu-20.04-focal-ppc64le"
|
||||
- docker: "ubuntu-22.04-jammy-ppc64le"
|
||||
qemu-arch: "ppc64le"
|
||||
- docker: "ubuntu-20.04-focal-s390x"
|
||||
- docker: "ubuntu-22.04-jammy-s390x"
|
||||
qemu-arch: "s390x"
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
|
8
.github/workflows/test-windows.yml
vendored
|
@ -8,7 +8,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
|
||||
architecture: ["x86", "x64"]
|
||||
include:
|
||||
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
|
||||
|
@ -41,10 +41,10 @@ jobs:
|
|||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
- name: Print build system information
|
||||
run: python .github/workflows/system-info.py
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
- name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
|
||||
|
||||
- name: Install dependencies
|
||||
id: install
|
||||
|
|
3
.github/workflows/test.yml
vendored
|
@ -15,6 +15,7 @@ jobs:
|
|||
python-version: [
|
||||
"pypy-3.8",
|
||||
"pypy-3.7",
|
||||
"3.11-dev",
|
||||
"3.10",
|
||||
"3.9",
|
||||
"3.8",
|
||||
|
@ -59,6 +60,8 @@ jobs:
|
|||
if: startsWith(matrix.os, 'macOS')
|
||||
run: |
|
||||
.github/workflows/macos-install.sh
|
||||
env:
|
||||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
|
2
.github/workflows/tidelift.yml
vendored
|
@ -4,9 +4,11 @@ on:
|
|||
- cron: "30 2 * * *" # daily at 02:30 UTC
|
||||
push:
|
||||
paths:
|
||||
- "Pipfile*"
|
||||
- ".github/workflows/tidelift.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "Pipfile*"
|
||||
- ".github/workflows/tidelift.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ repos:
|
|||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.1.13
|
||||
rev: v1.2.0
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
|
||||
|
@ -37,10 +37,15 @@ repos:
|
|||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.6
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
autoupdate_schedule: monthly
|
||||
|
|
2563
CHANGES.rst
2
Makefile
|
@ -85,6 +85,8 @@ release-test:
|
|||
sdist:
|
||||
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
|
||||
python3 -m build --sdist
|
||||
python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
|
||||
python3 -m twine check --strict dist/*
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
|
|
|
@ -36,6 +36,9 @@ As of 2019, Pillow development is
|
|||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
|
||||
alt="GitHub Actions build status (Test MinGW)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
|
||||
alt="GitHub Actions build status (Test Cygwin)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
|
||||
alt="GitHub Actions build status (Test Docker)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
|
||||
|
|
|
@ -24,7 +24,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
|
@ -61,7 +60,6 @@ Released as needed for security, installation or critical bug fixes.
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Check and upload all binaries and source distributions e.g.:
|
||||
|
@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related
|
|||
* [ ] Create and check source distribution:
|
||||
```bash
|
||||
make sdist
|
||||
python3 -m twine check --strict dist/*
|
||||
```
|
||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
|
||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
|
||||
|
|
|
@ -8,7 +8,7 @@ Dependencies
|
|||
|
||||
Install::
|
||||
|
||||
python3 -m pip install pytest pytest-cov
|
||||
python3 -m pip install pytest pytest-cov pytest-timeout
|
||||
|
||||
Execution
|
||||
---------
|
||||
|
|
|
@ -324,7 +324,7 @@ def is_mingw():
|
|||
return sysconfig.get_platform() == "mingw"
|
||||
|
||||
|
||||
class cached_property:
|
||||
class CachedProperty:
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
|
|
BIN
Tests/images/comment_after_last_frame.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/cross_scan_line_truncated.tga
Normal file
Before Width: | Height: | Size: 58 B After Width: | Height: | Size: 198 B |
BIN
Tests/images/duplicate_number_of_loops.gif
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/hopper_16bit.pgm
Normal file
4
Tests/images/hopper_16bit_plain.pgm
Normal file
BIN
Tests/images/hopper_1bit.pbm
Normal file
14
Tests/images/hopper_1bit_plain.pbm
Normal file
BIN
Tests/images/hopper_8bit.pgm
Normal file
BIN
Tests/images/hopper_8bit.ppm
Normal file
4
Tests/images/hopper_8bit_plain.pgm
Normal file
BIN
Tests/images/hopper_8bit_plain.ppm
Normal file
BIN
Tests/images/imagedraw_polygon_1px_high_translucent.png
Normal file
After Width: | Height: | Size: 76 B |
BIN
Tests/images/issue_6194.j2k
Normal file
BIN
Tests/images/multiple_comments.gif
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Tests/images/second_frame_comment.gif
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
Tests/images/tiff_wrong_bits_per_sample_3.tiff
Normal file
BIN
Tests/images/tiny.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
Tests/images/zero_height.j2k
Normal file
|
@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1):
|
|||
return image._new(image.im.box_blur(radius, n))
|
||||
|
||||
|
||||
def assertImage(im, data, delta=0):
|
||||
def assert_image(im, data, delta=0):
|
||||
it = iter(im.getdata())
|
||||
for data_row in data:
|
||||
im_row = [next(it) for _ in range(im.size[0])]
|
||||
|
@ -35,12 +35,12 @@ def assertImage(im, data, delta=0):
|
|||
next(it)
|
||||
|
||||
|
||||
def assertBlur(im, radius, data, passes=1, delta=0):
|
||||
def assert_blur(im, radius, data, passes=1, delta=0):
|
||||
# check grayscale image
|
||||
assertImage(box_blur(im, radius, passes), data, delta)
|
||||
assert_image(box_blur(im, radius, passes), data, delta)
|
||||
rgba = Image.merge("RGBA", (im, im, im, im))
|
||||
for band in box_blur(rgba, radius, passes).split():
|
||||
assertImage(band, data, delta)
|
||||
assert_image(band, data, delta)
|
||||
|
||||
|
||||
def test_color_modes():
|
||||
|
@ -64,7 +64,7 @@ def test_color_modes():
|
|||
|
||||
|
||||
def test_radius_0():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0,
|
||||
[
|
||||
|
@ -80,7 +80,7 @@ def test_radius_0():
|
|||
|
||||
|
||||
def test_radius_0_02():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.02,
|
||||
[
|
||||
|
@ -97,7 +97,7 @@ def test_radius_0_02():
|
|||
|
||||
|
||||
def test_radius_0_05():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.05,
|
||||
[
|
||||
|
@ -114,7 +114,7 @@ def test_radius_0_05():
|
|||
|
||||
|
||||
def test_radius_0_1():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.1,
|
||||
[
|
||||
|
@ -131,7 +131,7 @@ def test_radius_0_1():
|
|||
|
||||
|
||||
def test_radius_0_5():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
0.5,
|
||||
[
|
||||
|
@ -148,7 +148,7 @@ def test_radius_0_5():
|
|||
|
||||
|
||||
def test_radius_1():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
@ -165,7 +165,7 @@ def test_radius_1():
|
|||
|
||||
|
||||
def test_radius_1_5():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1.5,
|
||||
[
|
||||
|
@ -182,7 +182,7 @@ def test_radius_1_5():
|
|||
|
||||
|
||||
def test_radius_bigger_then_half():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
3,
|
||||
[
|
||||
|
@ -199,7 +199,7 @@ def test_radius_bigger_then_half():
|
|||
|
||||
|
||||
def test_radius_bigger_then_width():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
10,
|
||||
[
|
||||
|
@ -214,7 +214,7 @@ def test_radius_bigger_then_width():
|
|||
|
||||
|
||||
def test_extreme_large_radius():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
600,
|
||||
[
|
||||
|
@ -229,7 +229,7 @@ def test_extreme_large_radius():
|
|||
|
||||
|
||||
def test_two_passes():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
@ -247,7 +247,7 @@ def test_two_passes():
|
|||
|
||||
|
||||
def test_three_passes():
|
||||
assertBlur(
|
||||
assert_blur(
|
||||
sample,
|
||||
1,
|
||||
[
|
||||
|
|
|
@ -15,27 +15,27 @@ except ImportError:
|
|||
class TestColorLut3DCoreAPI:
|
||||
def generate_identity_table(self, channels, size):
|
||||
if isinstance(size, tuple):
|
||||
size1D, size2D, size3D = size
|
||||
size_1d, size_2d, size_3d = size
|
||||
else:
|
||||
size1D, size2D, size3D = (size, size, size)
|
||||
size_1d, size_2d, size_3d = (size, size, size)
|
||||
|
||||
table = [
|
||||
[
|
||||
r / (size1D - 1) if size1D != 1 else 0,
|
||||
g / (size2D - 1) if size2D != 1 else 0,
|
||||
b / (size3D - 1) if size3D != 1 else 0,
|
||||
r / (size1D - 1) if size1D != 1 else 0,
|
||||
g / (size2D - 1) if size2D != 1 else 0,
|
||||
r / (size_1d - 1) if size_1d != 1 else 0,
|
||||
g / (size_2d - 1) if size_2d != 1 else 0,
|
||||
b / (size_3d - 1) if size_3d != 1 else 0,
|
||||
r / (size_1d - 1) if size_1d != 1 else 0,
|
||||
g / (size_2d - 1) if size_2d != 1 else 0,
|
||||
][:channels]
|
||||
for b in range(size3D)
|
||||
for g in range(size2D)
|
||||
for r in range(size1D)
|
||||
for b in range(size_3d)
|
||||
for g in range(size_2d)
|
||||
for r in range(size_1d)
|
||||
]
|
||||
return (
|
||||
channels,
|
||||
size1D,
|
||||
size2D,
|
||||
size3D,
|
||||
size_1d,
|
||||
size_2d,
|
||||
size_3d,
|
||||
[item for sublist in table for item in sublist],
|
||||
)
|
||||
|
||||
|
@ -567,7 +567,7 @@ class TestTransformColorLut3D:
|
|||
assert tuple(lut.size) == tuple(source.size)
|
||||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
assert lut.table[0:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
|
||||
assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
|
||||
|
||||
def test_3_to_4_channels(self):
|
||||
source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
|
||||
|
@ -576,7 +576,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) != len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:16] == [
|
||||
assert lut.table[:16] == [
|
||||
0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1,
|
||||
0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]
|
||||
# fmt: on
|
||||
|
@ -592,7 +592,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) != len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:18] == [
|
||||
assert lut.table[:18] == [
|
||||
1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0,
|
||||
1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]
|
||||
# fmt: on
|
||||
|
@ -606,7 +606,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:16] == [
|
||||
assert lut.table[:16] == [
|
||||
0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5,
|
||||
0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]
|
||||
# fmt: on
|
||||
|
@ -622,7 +622,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:18] == [
|
||||
assert lut.table[:18] == [
|
||||
0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0,
|
||||
0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]
|
||||
# fmt: on
|
||||
|
@ -639,7 +639,7 @@ class TestTransformColorLut3D:
|
|||
assert len(lut.table) == len(source.table)
|
||||
assert lut.table != source.table
|
||||
# fmt: off
|
||||
assert lut.table[0:16] == [
|
||||
assert lut.table[:16] == [
|
||||
0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5,
|
||||
0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5]
|
||||
# fmt: on
|
||||
|
|
|
@ -51,7 +51,6 @@ class TestDecompressionBomb:
|
|||
with Image.open(TEST_FILE):
|
||||
pass
|
||||
|
||||
@pytest.mark.xfail(reason="different exception")
|
||||
def test_exception_ico(self):
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
with Image.open("Tests/images/decompression_bomb.ico"):
|
||||
|
@ -70,15 +69,15 @@ class TestDecompressionBomb:
|
|||
|
||||
class TestDecompressionCrop:
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
def setup_class(cls):
|
||||
width, height = 128, 128
|
||||
Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
|
||||
|
||||
@classmethod
|
||||
def teardown_class(self):
|
||||
def teardown_class(cls):
|
||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||
|
||||
def testEnlargeCrop(self):
|
||||
def test_enlarge_crop(self):
|
||||
# Crops can extend the extents, therefore we should have the
|
||||
# same decompression bomb warnings on them.
|
||||
with hopper() as src:
|
||||
|
|
91
Tests/test_deprecate.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
import pytest
|
||||
|
||||
from PIL import _deprecate
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"version, expected",
|
||||
[
|
||||
(
|
||||
10,
|
||||
"Old thing is deprecated and will be removed in Pillow 10 "
|
||||
r"\(2023-07-01\)\. Use new thing instead\.",
|
||||
),
|
||||
(
|
||||
None,
|
||||
r"Old thing is deprecated and will be removed in a future version\. "
|
||||
r"Use new thing instead\.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_version(version, expected):
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", version, "new thing")
|
||||
|
||||
|
||||
def test_unknown_version():
|
||||
expected = r"Unknown removal version, update PIL\._deprecate\?"
|
||||
with pytest.raises(ValueError, match=expected):
|
||||
_deprecate.deprecate("Old thing", 12345, "new thing")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"deprecated, plural, expected",
|
||||
[
|
||||
(
|
||||
"Old thing",
|
||||
False,
|
||||
r"Old thing is deprecated and should be removed\.",
|
||||
),
|
||||
(
|
||||
"Old things",
|
||||
True,
|
||||
r"Old things are deprecated and should be removed\.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_old_version(deprecated, plural, expected):
|
||||
expected = r""
|
||||
with pytest.raises(RuntimeError, match=expected):
|
||||
_deprecate.deprecate(deprecated, 1, plural=plural)
|
||||
|
||||
|
||||
def test_plural():
|
||||
expected = (
|
||||
r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
|
||||
r"Use new thing instead\."
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old things", 10, "new thing", plural=True)
|
||||
|
||||
|
||||
def test_replacement_and_action():
|
||||
expected = "Use only one of 'replacement' and 'action'"
|
||||
with pytest.raises(ValueError, match=expected):
|
||||
_deprecate.deprecate(
|
||||
"Old thing", 10, replacement="new thing", action="Upgrade to new thing"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"action",
|
||||
[
|
||||
"Upgrade to new thing",
|
||||
"Upgrade to new thing.",
|
||||
],
|
||||
)
|
||||
def test_action(action):
|
||||
expected = (
|
||||
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
|
||||
r"Upgrade to new thing\."
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", 10, action=action)
|
||||
|
||||
|
||||
def test_no_replacement_or_action():
|
||||
expected = (
|
||||
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)"
|
||||
)
|
||||
with pytest.warns(DeprecationWarning, match=expected):
|
||||
_deprecate.deprecate("Old thing", 10)
|
18
Tests/test_deprecated_imageqt.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import warnings
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Arrange: cause all warnings to always be triggered
|
||||
warnings.simplefilter("always")
|
||||
|
||||
# Act: trigger a warning with Qt5
|
||||
from PIL import ImageQt
|
||||
|
||||
|
||||
def test_deprecated():
|
||||
# Assert
|
||||
if ImageQt.qt_version in ("5", "side2"):
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[0].category, DeprecationWarning)
|
||||
assert "deprecated" in str(w[0].message)
|
||||
else:
|
||||
assert len(w) == 0
|
|
@ -637,6 +637,15 @@ def test_apng_save_blend(tmp_path):
|
|||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open("Tests/images/apng/delay.png")
|
||||
im.seek(1)
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(0)
|
||||
|
||||
|
||||
def test_constants_deprecation():
|
||||
for enum, prefix in {
|
||||
PngImagePlugin.Disposal: "APNG_DISPOSE_",
|
||||
|
|
|
@ -46,6 +46,15 @@ def test_closed_file():
|
|||
im.close()
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open(animated_test_file)
|
||||
im.seek(1)
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(0)
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(static_test_file) as im:
|
||||
|
|
|
@ -3,7 +3,7 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features
|
||||
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -46,6 +46,19 @@ def test_closed_file():
|
|||
im.close()
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open("Tests/images/iss634.gif")
|
||||
im.load()
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.is_animated
|
||||
with pytest.raises(ValueError):
|
||||
im.n_frames
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(1)
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(TEST_GIF) as im:
|
||||
|
@ -341,16 +354,23 @@ def test_seek_rewind():
|
|||
assert_image_equal(im, expected)
|
||||
|
||||
|
||||
def test_n_frames():
|
||||
for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]:
|
||||
# Test is_animated before n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
@pytest.mark.parametrize(
|
||||
"path, n_frames",
|
||||
(
|
||||
(TEST_GIF, 1),
|
||||
("Tests/images/comment_after_last_frame.gif", 2),
|
||||
("Tests/images/iss634.gif", 42),
|
||||
),
|
||||
)
|
||||
def test_n_frames(path, n_frames):
|
||||
# Test is_animated before n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
# Test is_animated after n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
# Test is_animated after n_frames
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
|
||||
def test_no_change():
|
||||
|
@ -619,7 +639,8 @@ def test_dispose2_background(tmp_path):
|
|||
assert im.getpixel((0, 0)) == (255, 0, 0)
|
||||
|
||||
|
||||
def test_transparency_in_second_frame():
|
||||
def test_transparency_in_second_frame(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||
assert im.info["transparency"] == 0
|
||||
|
||||
|
@ -629,6 +650,14 @@ def test_transparency_in_second_frame():
|
|||
|
||||
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
|
||||
|
||||
im.save(out, save_all=True)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
reread.seek(reread.tell() + 1)
|
||||
assert_image_equal_tofile(
|
||||
reread, "Tests/images/different_transparency_merged.png"
|
||||
)
|
||||
|
||||
|
||||
def test_no_transparency_in_second_frame():
|
||||
with Image.open("Tests/images/iss634.gif") as img:
|
||||
|
@ -640,6 +669,22 @@ def test_no_transparency_in_second_frame():
|
|||
assert img.histogram()[255] == 0
|
||||
|
||||
|
||||
def test_remapped_transparency(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
im = Image.new("P", (1, 2))
|
||||
im2 = im.copy()
|
||||
|
||||
# Add transparency at a higher index
|
||||
# so that it will be optimized to a lower index
|
||||
im.putpixel((0, 1), 5)
|
||||
im.info["transparency"] = 5
|
||||
im.save(out, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
|
||||
|
||||
|
||||
def test_duration(tmp_path):
|
||||
duration = 1000
|
||||
|
||||
|
@ -691,6 +736,23 @@ def test_multiple_duration(tmp_path):
|
|||
pass
|
||||
|
||||
|
||||
def test_roundtrip_info_duration(tmp_path):
|
||||
duration_list = [100, 500, 500]
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as im:
|
||||
assert [
|
||||
frame.info["duration"] for frame in ImageSequence.Iterator(im)
|
||||
] == duration_list
|
||||
|
||||
im.save(out, save_all=True)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert [
|
||||
frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
|
||||
] == duration_list
|
||||
|
||||
|
||||
def test_identical_frames(tmp_path):
|
||||
duration_list = [1000, 1500, 2000, 4000]
|
||||
|
||||
|
@ -742,9 +804,16 @@ def test_number_of_loops(tmp_path):
|
|||
im = Image.new("L", (100, 100), "#000")
|
||||
im.save(out, loop=number_of_loops)
|
||||
with Image.open(out) as reread:
|
||||
|
||||
assert reread.info["loop"] == number_of_loops
|
||||
|
||||
# Check that even if a subsequent GIF frame has the number of loops specified,
|
||||
# only the value from the first frame is used
|
||||
with Image.open("Tests/images/duplicate_number_of_loops.gif") as im:
|
||||
assert im.info["loop"] == 2
|
||||
|
||||
im.seek(1)
|
||||
assert im.info["loop"] == 2
|
||||
|
||||
|
||||
def test_background(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
@ -777,6 +846,9 @@ def test_comment(tmp_path):
|
|||
with Image.open(out) as reread:
|
||||
assert reread.info["comment"] == im.info["comment"].encode()
|
||||
|
||||
# Test that GIF89a is used for comments
|
||||
assert reread.info["version"] == b"GIF89a"
|
||||
|
||||
|
||||
def test_comment_over_255(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
@ -787,43 +859,95 @@ def test_comment_over_255(tmp_path):
|
|||
im.info["comment"] = comment
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
|
||||
assert reread.info["comment"] == comment
|
||||
|
||||
# Test that GIF89a is used for comments
|
||||
assert reread.info["version"] == b"GIF89a"
|
||||
|
||||
|
||||
def test_zero_comment_subblocks():
|
||||
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
|
||||
assert_image_equal_tofile(im, TEST_GIF)
|
||||
|
||||
|
||||
def test_read_multiple_comment_blocks():
|
||||
with Image.open("Tests/images/multiple_comments.gif") as im:
|
||||
# Multiple comment blocks in a frame are separated not concatenated
|
||||
assert im.info["comment"] == b"Test comment 1\nTest comment 2"
|
||||
|
||||
|
||||
def test_empty_string_comment(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert "comment" in im.info
|
||||
|
||||
# Empty string comment should suppress existing comment
|
||||
im.save(out, save_all=True, comment="")
|
||||
|
||||
with Image.open(out) as reread:
|
||||
for frame in ImageSequence.Iterator(reread):
|
||||
assert "comment" not in frame.info
|
||||
|
||||
|
||||
def test_retain_comment_in_subsequent_frames(tmp_path):
|
||||
# Test that a comment block at the beginning is kept
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
assert frame.info["comment"] == b"Created with GIMP"
|
||||
|
||||
with Image.open("Tests/images/second_frame_comment.gif") as im:
|
||||
assert "comment" not in im.info
|
||||
|
||||
# Test that a comment in the middle is read
|
||||
im.seek(1)
|
||||
assert im.info["comment"] == b"Comment in the second frame"
|
||||
|
||||
# Test that it is still present in a later frame
|
||||
im.seek(2)
|
||||
assert im.info["comment"] == b"Comment in the second frame"
|
||||
|
||||
# Test that rewinding removes the comment
|
||||
im.seek(0)
|
||||
assert "comment" not in im.info
|
||||
|
||||
# Test that a saved image keeps the comment
|
||||
out = str(tmp_path / "temp.gif")
|
||||
with Image.open("Tests/images/dispose_prev.gif") as im:
|
||||
im.save(out, save_all=True, comment="Test")
|
||||
|
||||
with Image.open(out) as reread:
|
||||
for frame in ImageSequence.Iterator(reread):
|
||||
assert frame.info["comment"] == b"Test"
|
||||
|
||||
|
||||
def test_version(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
def assertVersionAfterSave(im, version):
|
||||
def assert_version_after_save(im, version):
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["version"] == version
|
||||
|
||||
# Test that GIF87a is used by default
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
# Test setting the version to 89a
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
im.info["version"] = b"89a"
|
||||
assertVersionAfterSave(im, b"GIF89a")
|
||||
assert_version_after_save(im, b"GIF89a")
|
||||
|
||||
# Test that adding a GIF89a feature changes the version
|
||||
im.info["transparency"] = 1
|
||||
assertVersionAfterSave(im, b"GIF89a")
|
||||
assert_version_after_save(im, b"GIF89a")
|
||||
|
||||
# Test that a GIF87a image is also saved in that format
|
||||
with Image.open("Tests/images/test.colors.gif") as im:
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
# Test that a GIF89a image is also saved in that format
|
||||
im.info["version"] = b"GIF89a"
|
||||
assertVersionAfterSave(im, b"GIF87a")
|
||||
assert_version_after_save(im, b"GIF87a")
|
||||
|
||||
|
||||
def test_append_images(tmp_path):
|
||||
|
@ -838,10 +962,10 @@ def test_append_images(tmp_path):
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
im.save(out, save_all=True, append_images=imGenerator(ims))
|
||||
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert reread.n_frames == 3
|
||||
|
|
|
@ -4,15 +4,13 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import IcnsImagePlugin, Image, _binary, features
|
||||
from PIL import IcnsImagePlugin, Image, _binary
|
||||
|
||||
from .helper import assert_image_equal, assert_image_similar_tofile
|
||||
from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless_feature
|
||||
|
||||
# sample icon file
|
||||
TEST_FILE = "Tests/images/pillow.icns"
|
||||
|
||||
ENABLE_JPEG2K = features.check_codec("jpg_2000")
|
||||
|
||||
|
||||
def test_sanity():
|
||||
# Loading this icon by default should result in the largest size
|
||||
|
@ -111,14 +109,12 @@ def test_older_icon():
|
|||
assert im2.size == (wr, hr)
|
||||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
def test_jp2_icon():
|
||||
# This icon uses JPEG 2000 images instead of the PNG images.
|
||||
# The advantage of doing this is that OS X 10.5 supports JPEG 2000
|
||||
# but not PNG; some commercial software therefore does just this.
|
||||
|
||||
if not ENABLE_JPEG2K:
|
||||
return
|
||||
|
||||
with Image.open("Tests/images/pillow3.icns") as im:
|
||||
for w, h, r in im.info["sizes"]:
|
||||
wr = w * r
|
||||
|
@ -149,6 +145,7 @@ def test_not_an_icns_file():
|
|||
IcnsImagePlugin.IcnsFile(fp)
|
||||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
def test_icns_decompression_bomb():
|
||||
with Image.open(
|
||||
"Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns"
|
||||
|
|
|
@ -754,7 +754,7 @@ class TestFileJpeg:
|
|||
|
||||
# Act / Assert
|
||||
# "When the image resolution is unknown, 72 [dpi] is designated."
|
||||
# http://www.exiv2.org/tags.html
|
||||
# https://exiv2.org/tags.html
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_invalid_exif(self):
|
||||
|
|
|
@ -298,6 +298,11 @@ def test_16bit_jp2_roundtrips():
|
|||
assert_image_equal(im, jp2)
|
||||
|
||||
|
||||
def test_issue_6194():
|
||||
with Image.open("Tests/images/issue_6194.j2k") as im:
|
||||
assert im.getpixel((5, 5)) == 31
|
||||
|
||||
|
||||
def test_unbound_local():
|
||||
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
|
||||
with pytest.raises(OSError):
|
||||
|
|
|
@ -497,8 +497,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im.save(out, compression="tiff_adobe_deflate")
|
||||
assert_image_equal_tofile(im, out)
|
||||
|
||||
def test_palette_save(self, tmp_path):
|
||||
im = hopper("P")
|
||||
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||
def test_palette_save(self, im, tmp_path):
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
|
|
|
@ -48,6 +48,14 @@ def test_closed_file():
|
|||
im.close()
|
||||
|
||||
|
||||
def test_seek_after_close():
|
||||
im = Image.open(test_files[0])
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(1)
|
||||
|
||||
|
||||
def test_context_manager():
|
||||
with warnings.catch_warnings():
|
||||
with Image.open(test_files[0]) as im:
|
||||
|
@ -116,6 +124,15 @@ def test_parallax():
|
|||
assert exif.get_ifd(0x927C)[0xB211] == -3.125
|
||||
|
||||
|
||||
def test_reload_exif_after_seek():
|
||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||
exif = im.getexif()
|
||||
del exif[296]
|
||||
|
||||
im.seek(1)
|
||||
assert 296 in exif
|
||||
|
||||
|
||||
def test_mp():
|
||||
for test_file in test_files:
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -145,10 +162,10 @@ def test_mp_attribute():
|
|||
for test_file in test_files:
|
||||
with Image.open(test_file) as im:
|
||||
mpinfo = im._getmp()
|
||||
frameNumber = 0
|
||||
frame_number = 0
|
||||
for mpentry in mpinfo[0xB002]:
|
||||
mpattr = mpentry["Attribute"]
|
||||
if frameNumber:
|
||||
if frame_number:
|
||||
assert not mpattr["RepresentativeImageFlag"]
|
||||
else:
|
||||
assert mpattr["RepresentativeImageFlag"]
|
||||
|
@ -157,7 +174,7 @@ def test_mp_attribute():
|
|||
assert mpattr["ImageDataFormat"] == "JPEG"
|
||||
assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
|
||||
assert mpattr["Reserved"] == 0
|
||||
frameNumber += 1
|
||||
frame_number += 1
|
||||
|
||||
|
||||
def test_seek():
|
||||
|
|
|
@ -131,10 +131,10 @@ def test_save_all(tmp_path):
|
|||
assert os.path.getsize(outfile) > 0
|
||||
|
||||
# Test appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
im.save(outfile, save_all=True, append_images=imGenerator(ims))
|
||||
im.save(outfile, save_all=True, append_images=im_generator(ims))
|
||||
|
||||
assert os.path.isfile(outfile)
|
||||
assert os.path.getsize(outfile) > 0
|
||||
|
@ -253,9 +253,9 @@ def test_pdf_append(tmp_path):
|
|||
check_pdf_pages_consistency(pdf)
|
||||
|
||||
# append two images
|
||||
mode_CMYK = hopper("CMYK")
|
||||
mode_P = hopper("P")
|
||||
mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P])
|
||||
mode_cmyk = hopper("CMYK")
|
||||
mode_p = hopper("P")
|
||||
mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p])
|
||||
|
||||
# open the PDF again, check pages and info again
|
||||
with PdfParser.PdfParser(pdf_filename) as pdf:
|
||||
|
|
|
@ -643,6 +643,17 @@ class TestFilePng:
|
|||
|
||||
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
|
||||
|
||||
@pytest.mark.parametrize("cid", (b"IHDR", b"pHYs", b"acTL", b"fcTL", b"fdAT"))
|
||||
def test_truncated_chunks(self, cid):
|
||||
fp = BytesIO()
|
||||
with PngImagePlugin.PngStream(fp) as png:
|
||||
with pytest.raises(ValueError):
|
||||
png.call(cid, 0, 0)
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
png.call(cid, 0, 0)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
def test_specify_bits(self, tmp_path):
|
||||
im = hopper("P")
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL import Image, PpmImagePlugin
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
|
||||
|
||||
|
@ -22,6 +22,21 @@ def test_sanity():
|
|||
@pytest.mark.parametrize(
|
||||
"data, mode, pixels",
|
||||
(
|
||||
(b"P2 3 1 4 0 2 4", "L", (0, 128, 255)),
|
||||
(b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)),
|
||||
# P3 with maxval < 255
|
||||
(
|
||||
b"P3 3 1 17 0 1 2 8 9 10 15 16 17",
|
||||
"RGB",
|
||||
((0, 15, 30), (120, 135, 150), (225, 240, 255)),
|
||||
),
|
||||
# P3 with maxval > 255
|
||||
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
||||
(
|
||||
b"P3 3 1 257 0 1 2 128 129 130 256 257 257",
|
||||
"RGB",
|
||||
((0, 1, 2), (127, 128, 129), (254, 255, 255)),
|
||||
),
|
||||
(b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
|
||||
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
||||
# P6 with maxval < 255
|
||||
|
@ -35,7 +50,6 @@ def test_sanity():
|
|||
),
|
||||
),
|
||||
# P6 with maxval > 255
|
||||
# Scale down to 255, since there is no RGB mode with more than 8-bit
|
||||
(
|
||||
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
||||
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
||||
|
@ -85,14 +99,111 @@ def test_pnm(tmp_path):
|
|||
assert_image_equal_tofile(im, f)
|
||||
|
||||
|
||||
def test_magic(tmp_path):
|
||||
@pytest.mark.parametrize(
|
||||
"plain_path, raw_path",
|
||||
(
|
||||
(
|
||||
"Tests/images/hopper_1bit_plain.pbm", # P1
|
||||
"Tests/images/hopper_1bit.pbm", # P4
|
||||
),
|
||||
(
|
||||
"Tests/images/hopper_8bit_plain.pgm", # P2
|
||||
"Tests/images/hopper_8bit.pgm", # P5
|
||||
),
|
||||
(
|
||||
"Tests/images/hopper_8bit_plain.ppm", # P3
|
||||
"Tests/images/hopper_8bit.ppm", # P6
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_plain(plain_path, raw_path):
|
||||
with Image.open(plain_path) as im:
|
||||
assert_image_equal_tofile(im, raw_path)
|
||||
|
||||
|
||||
def test_16bit_plain_pgm():
|
||||
# P2 with maxval 2 ** 16 - 1
|
||||
with Image.open("Tests/images/hopper_16bit_plain.pgm") as im:
|
||||
assert im.mode == "I"
|
||||
assert im.size == (128, 128)
|
||||
assert im.get_format_mimetype() == "image/x-portable-graymap"
|
||||
|
||||
# P5 with maxval 2 ** 16 - 1
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"header, data, comment_count",
|
||||
(
|
||||
(b"P1\n2 2", b"1010", 10**6),
|
||||
(b"P2\n3 1\n4", b"0 2 4", 1),
|
||||
(b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6),
|
||||
),
|
||||
)
|
||||
def test_plain_data_with_comment(tmp_path, header, data, comment_count):
|
||||
path1 = str(tmp_path / "temp1.ppm")
|
||||
path2 = str(tmp_path / "temp2.ppm")
|
||||
comment = b"# comment" * comment_count
|
||||
with open(path1, "wb") as f1, open(path2, "wb") as f2:
|
||||
f1.write(header + b"\n\n" + data)
|
||||
f2.write(header + b"\n" + comment + b"\n" + data + comment)
|
||||
|
||||
with Image.open(path1) as im:
|
||||
assert_image_equal_tofile(im, path2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
|
||||
def test_plain_truncated_data(tmp_path, data):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"PyInvalid")
|
||||
f.write(data)
|
||||
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(path):
|
||||
pass
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
|
||||
def test_plain_invalid_data(tmp_path, data):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
(
|
||||
b"P3\n128 128\n255\n012345678910", # half token too long
|
||||
b"P3\n128 128\n255\n012345678910 0", # token too long
|
||||
),
|
||||
)
|
||||
def test_plain_ppm_token_too_long(tmp_path, data):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
with Image.open(path) as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_plain_ppm_value_too_large(tmp_path):
|
||||
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):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_magic():
|
||||
with pytest.raises(SyntaxError):
|
||||
PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid"))
|
||||
|
||||
|
||||
def test_header_with_comments(tmp_path):
|
||||
|
@ -114,7 +225,7 @@ def test_non_integer_token(tmp_path):
|
|||
pass
|
||||
|
||||
|
||||
def test_token_too_long(tmp_path):
|
||||
def test_header_token_too_long(tmp_path):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P6\n 01234567890")
|
||||
|
@ -123,7 +234,7 @@ def test_token_too_long(tmp_path):
|
|||
with Image.open(path):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "Token too long in file header: b'01234567890'"
|
||||
assert str(e.value) == "Token too long in file header: 01234567890"
|
||||
|
||||
|
||||
def test_truncated_file(tmp_path):
|
||||
|
@ -145,6 +256,19 @@ def test_truncated_file(tmp_path):
|
|||
im.load()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("maxval", (0, 65536))
|
||||
def test_invalid_maxval(maxval, tmp_path):
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
with open(path, "w") as f:
|
||||
f.write("P6\n3 1 " + str(maxval))
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
with Image.open(path):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
|
||||
|
||||
|
||||
def test_neg_ppm():
|
||||
# Storage.c accepted negative values for xsize, ysize. the
|
||||
# internal open_ppm function didn't check for sanity but it
|
||||
|
|
|
@ -101,6 +101,10 @@ def test_cross_scan_line():
|
|||
with Image.open("Tests/images/cross_scan_line.tga") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
|
||||
|
||||
with Image.open("Tests/images/cross_scan_line_truncated.tga") as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
|
||||
def test_save(tmp_path):
|
||||
test_file = "Tests/images/tga_id_field.tga"
|
||||
|
|
|
@ -70,6 +70,15 @@ class TestFileTiff:
|
|||
im.load()
|
||||
im.close()
|
||||
|
||||
def test_seek_after_close(self):
|
||||
im = Image.open("Tests/images/multipage.tiff")
|
||||
im.close()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.n_frames
|
||||
with pytest.raises(ValueError):
|
||||
im.seek(1)
|
||||
|
||||
def test_context_manager(self):
|
||||
with warnings.catch_warnings():
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
|
@ -92,17 +101,33 @@ class TestFileTiff:
|
|||
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file_name,mode,size,offset",
|
||||
"file_name,mode,size,tile",
|
||||
[
|
||||
("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160),
|
||||
("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8),
|
||||
(
|
||||
"tiff_wrong_bits_per_sample.tiff",
|
||||
"RGBA",
|
||||
(52, 53),
|
||||
[("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
|
||||
),
|
||||
(
|
||||
"tiff_wrong_bits_per_sample_2.tiff",
|
||||
"RGB",
|
||||
(16, 16),
|
||||
[("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
|
||||
),
|
||||
(
|
||||
"tiff_wrong_bits_per_sample_3.tiff",
|
||||
"RGBA",
|
||||
(512, 256),
|
||||
[("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_wrong_bits_per_sample(self, file_name, mode, size, offset):
|
||||
def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
|
||||
with Image.open("Tests/images/" + file_name) as im:
|
||||
assert im.mode == mode
|
||||
assert im.size == size
|
||||
assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))]
|
||||
assert im.tile == tile
|
||||
im.load()
|
||||
|
||||
def test_set_legacy_api(self):
|
||||
|
@ -151,14 +176,14 @@ class TestFileTiff:
|
|||
assert im.info["dpi"] == (71.0, 71.0)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resolutionUnit, dpi",
|
||||
"resolution_unit, dpi",
|
||||
[(None, 72.8), (2, 72.8), (3, 184.912)],
|
||||
)
|
||||
def test_load_float_dpi(self, resolutionUnit, dpi):
|
||||
def test_load_float_dpi(self, resolution_unit, dpi):
|
||||
with Image.open(
|
||||
"Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif"
|
||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||
) as im:
|
||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
|
||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
||||
assert im.info["dpi"] == (dpi, dpi)
|
||||
|
||||
def test_save_float_dpi(self, tmp_path):
|
||||
|
@ -472,6 +497,26 @@ class TestFileTiff:
|
|||
exif = im.getexif()
|
||||
check_exif(exif)
|
||||
|
||||
def test_modify_exif(self, tmp_path):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||
exif = im.getexif()
|
||||
exif[256] = 100
|
||||
|
||||
im.save(outfile, exif=exif)
|
||||
|
||||
with Image.open(outfile) as im:
|
||||
exif = im.getexif()
|
||||
assert exif[256] == 100
|
||||
|
||||
def test_reload_exif_after_seek(self):
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
exif = im.getexif()
|
||||
del exif[256]
|
||||
im.seek(1)
|
||||
|
||||
assert 256 in exif
|
||||
|
||||
def test_exif_frames(self):
|
||||
# Test that EXIF data can change across frames
|
||||
with Image.open("Tests/images/g4-multi.tiff") as im:
|
||||
|
@ -655,11 +700,11 @@ class TestFileTiff:
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Test appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
mp = BytesIO()
|
||||
im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims))
|
||||
im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
|
||||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
|
@ -690,6 +735,13 @@ class TestFileTiff:
|
|||
with Image.open(outfile) as reloaded:
|
||||
assert reloaded.info["icc_profile"] == icc_profile
|
||||
|
||||
def test_save_bmp_compression(self, tmp_path):
|
||||
with Image.open("Tests/images/hopper.bmp") as im:
|
||||
assert im.info["compression"] == 0
|
||||
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile)
|
||||
|
||||
def test_discard_icc_profile(self, tmp_path):
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
|
||||
|
|
|
@ -28,26 +28,26 @@ def test_rt_metadata(tmp_path):
|
|||
# For text items, we still have to decode('ascii','replace') because
|
||||
# the tiff file format can't take 8 bit bytes in that field.
|
||||
|
||||
basetextdata = "This is some arbitrary metadata for a text field"
|
||||
bindata = basetextdata.encode("ascii") + b" \xff"
|
||||
textdata = basetextdata + " " + chr(255)
|
||||
reloaded_textdata = basetextdata + " ?"
|
||||
floatdata = 12.345
|
||||
doubledata = 67.89
|
||||
base_text_data = "This is some arbitrary metadata for a text field"
|
||||
bin_data = base_text_data.encode("ascii") + b" \xff"
|
||||
text_data = base_text_data + " " + chr(255)
|
||||
reloaded_text_data = base_text_data + " ?"
|
||||
float_data = 12.345
|
||||
double_data = 67.89
|
||||
info = TiffImagePlugin.ImageFileDirectory()
|
||||
|
||||
ImageJMetaData = TAG_IDS["ImageJMetaData"]
|
||||
ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
|
||||
ImageDescription = TAG_IDS["ImageDescription"]
|
||||
|
||||
info[ImageJMetaDataByteCounts] = len(bindata)
|
||||
info[ImageJMetaData] = bindata
|
||||
info[TAG_IDS["RollAngle"]] = floatdata
|
||||
info[ImageJMetaDataByteCounts] = len(bin_data)
|
||||
info[ImageJMetaData] = bin_data
|
||||
info[TAG_IDS["RollAngle"]] = float_data
|
||||
info.tagtype[TAG_IDS["RollAngle"]] = 11
|
||||
info[TAG_IDS["YawAngle"]] = doubledata
|
||||
info[TAG_IDS["YawAngle"]] = double_data
|
||||
info.tagtype[TAG_IDS["YawAngle"]] = 12
|
||||
|
||||
info[ImageDescription] = textdata
|
||||
info[ImageDescription] = text_data
|
||||
|
||||
f = str(tmp_path / "temp.tif")
|
||||
|
||||
|
@ -55,28 +55,28 @@ def test_rt_metadata(tmp_path):
|
|||
|
||||
with Image.open(f) as loaded:
|
||||
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),)
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||
|
||||
assert loaded.tag[ImageJMetaData] == bindata
|
||||
assert loaded.tag_v2[ImageJMetaData] == bindata
|
||||
assert loaded.tag[ImageJMetaData] == bin_data
|
||||
assert loaded.tag_v2[ImageJMetaData] == bin_data
|
||||
|
||||
assert loaded.tag[ImageDescription] == (reloaded_textdata,)
|
||||
assert loaded.tag_v2[ImageDescription] == reloaded_textdata
|
||||
assert loaded.tag[ImageDescription] == (reloaded_text_data,)
|
||||
assert loaded.tag_v2[ImageDescription] == reloaded_text_data
|
||||
|
||||
loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
|
||||
assert round(abs(loaded_float - floatdata), 5) == 0
|
||||
assert round(abs(loaded_float - float_data), 5) == 0
|
||||
loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
|
||||
assert round(abs(loaded_double - doubledata), 7) == 0
|
||||
assert round(abs(loaded_double - double_data), 7) == 0
|
||||
|
||||
# check with 2 element ImageJMetaDataByteCounts, issue #2006
|
||||
|
||||
info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8)
|
||||
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
||||
img.save(f, tiffinfo=info)
|
||||
with Image.open(f) as loaded:
|
||||
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
|
||||
|
||||
def test_read_metadata():
|
||||
|
@ -356,7 +356,7 @@ def test_empty_values():
|
|||
assert 33432 in info
|
||||
|
||||
|
||||
def test_PhotoshopInfo(tmp_path):
|
||||
def test_photoshop_info(tmp_path):
|
||||
with Image.open("Tests/images/issue_2278.tif") as im:
|
||||
assert len(im.tag_v2[34377]) == 70
|
||||
assert isinstance(im.tag_v2[34377], bytes)
|
||||
|
|
|
@ -191,6 +191,17 @@ class TestFileWebp:
|
|||
Image.open(blob).load()
|
||||
Image.open(blob).load()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"background",
|
||||
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
|
||||
)
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_invalid_background(self, background, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = hopper()
|
||||
with pytest.raises(OSError):
|
||||
im.save(temp_file, save_all=True, append_images=[im], background=background)
|
||||
|
||||
@skip_unless_feature("webp_anim")
|
||||
def test_background_from_gif(self, tmp_path):
|
||||
# Save L mode GIF with background
|
||||
|
|
|
@ -90,14 +90,14 @@ def test_write_animation_RGB(tmp_path):
|
|||
check(temp_file1)
|
||||
|
||||
# Tests appending using a generator
|
||||
def imGenerator(ims):
|
||||
def im_generator(ims):
|
||||
yield from ims
|
||||
|
||||
temp_file2 = str(tmp_path / "temp_generator.webp")
|
||||
frame1.copy().save(
|
||||
temp_file2,
|
||||
save_all=True,
|
||||
append_images=imGenerator([frame2]),
|
||||
append_images=im_generator([frame2]),
|
||||
lossless=True,
|
||||
)
|
||||
check(temp_file2)
|
||||
|
|
|
@ -7,7 +7,7 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError
|
||||
from PIL import Image, ImageDraw, ImagePalette, TiffImagePlugin, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -159,6 +159,8 @@ class TestImage:
|
|||
assert im.size == (128, 128)
|
||||
|
||||
for ext in (".jpg", ".jp2"):
|
||||
if ext == ".jp2" and not features.check_codec("jpg_2000"):
|
||||
pytest.skip("jpg_2000 not available")
|
||||
temp_file = str(tmp_path / ("temp." + ext))
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
|
@ -168,7 +170,7 @@ class TestImage:
|
|||
temp_file = str(tmp_path / "temp.jpg")
|
||||
|
||||
class FP:
|
||||
def write(a, b):
|
||||
def write(self, b):
|
||||
pass
|
||||
|
||||
fp = FP()
|
||||
|
@ -661,11 +663,34 @@ class TestImage:
|
|||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
assert_image_equal(im, im.remap_palette(list(range(256))))
|
||||
|
||||
# Test identity transform with an RGBA palette
|
||||
im = Image.new("P", (256, 1))
|
||||
for x in range(256):
|
||||
im.putpixel((x, 0), x)
|
||||
im.putpalette(list(range(256)) * 4, "RGBA")
|
||||
im_remapped = im.remap_palette(list(range(256)))
|
||||
assert_image_equal(im, im_remapped)
|
||||
assert im.palette.palette == im_remapped.palette.palette
|
||||
|
||||
# Test illegal image mode
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.remap_palette(None)
|
||||
|
||||
def test_remap_palette_transparency(self):
|
||||
im = Image.new("P", (1, 2))
|
||||
im.putpixel((0, 1), 1)
|
||||
im.info["transparency"] = 0
|
||||
|
||||
im_remapped = im.remap_palette([1, 0])
|
||||
assert im_remapped.info["transparency"] == 1
|
||||
|
||||
# Test unused transparency
|
||||
im.info["transparency"] = 2
|
||||
|
||||
im_remapped = im.remap_palette([1, 0])
|
||||
assert "transparency" not in im_remapped.info
|
||||
|
||||
def test__new(self):
|
||||
im = hopper("RGB")
|
||||
im_p = hopper("P")
|
||||
|
@ -883,6 +908,35 @@ class TestImage:
|
|||
im = Image.new("RGB", size)
|
||||
assert im.tobytes() == b""
|
||||
|
||||
def test_apply_transparency(self):
|
||||
im = Image.new("P", (1, 1))
|
||||
im.putpalette((0, 0, 0, 1, 1, 1))
|
||||
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
|
||||
|
||||
# Test that no transformation is applied without transparency
|
||||
im.apply_transparency()
|
||||
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
|
||||
|
||||
# Test that a transparency index is applied
|
||||
im.info["transparency"] = 0
|
||||
im.apply_transparency()
|
||||
assert "transparency" not in im.info
|
||||
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1}
|
||||
|
||||
# Test that existing transparency is kept
|
||||
im = Image.new("P", (1, 1))
|
||||
im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
|
||||
im.info["transparency"] = 0
|
||||
im.apply_transparency()
|
||||
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1}
|
||||
|
||||
# Test that transparency bytes are applied
|
||||
with Image.open("Tests/images/pil123p.png") as im:
|
||||
assert isinstance(im.info["transparency"], bytes)
|
||||
assert im.palette.colors[(27, 35, 6)] == 24
|
||||
im.apply_transparency()
|
||||
assert im.palette.colors[(27, 35, 6, 214)] == 24
|
||||
|
||||
def test_categories_deprecation(self):
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert hopper().category == 0
|
||||
|
|
|
@ -80,3 +80,15 @@ def test_fromarray():
|
|||
with pytest.raises(TypeError):
|
||||
wrapped = Wrapper(test("L"), {"shape": (100, 128)})
|
||||
Image.fromarray(wrapped)
|
||||
|
||||
|
||||
def test_fromarray_palette():
|
||||
# Arrange
|
||||
i = im.convert("L")
|
||||
a = numpy.array(i)
|
||||
|
||||
# Act
|
||||
out = Image.fromarray(a, "P")
|
||||
|
||||
# Assert that the Python and C palettes match
|
||||
assert len(out.palette.colors) == len(out.im.getpalette()) / 3
|
||||
|
|
|
@ -27,15 +27,15 @@ def test_sanity():
|
|||
"HSV",
|
||||
)
|
||||
|
||||
for mode in modes:
|
||||
im = hopper(mode)
|
||||
for mode in modes:
|
||||
convert(im, mode)
|
||||
for input_mode in modes:
|
||||
im = hopper(input_mode)
|
||||
for output_mode in modes:
|
||||
convert(im, output_mode)
|
||||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0))
|
||||
for mode in modes:
|
||||
convert(im, mode)
|
||||
im = Image.new(input_mode, (0, 0))
|
||||
for output_mode in modes:
|
||||
convert(im, output_mode)
|
||||
|
||||
|
||||
def test_default():
|
||||
|
@ -222,6 +222,20 @@ def test_p_la():
|
|||
assert_image_similar(alpha, comparable, 5)
|
||||
|
||||
|
||||
def test_p2pa_alpha():
|
||||
with Image.open("Tests/images/tiny.png") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im_pa = im.convert("PA")
|
||||
assert im_pa.mode == "PA"
|
||||
|
||||
im_a = im_pa.getchannel("A")
|
||||
for x in range(4):
|
||||
alpha = 255 if x > 1 else 0
|
||||
for y in range(4):
|
||||
assert im_a.getpixel((x, y)) == alpha
|
||||
|
||||
|
||||
def test_matrix_illegal_conversion():
|
||||
# Arrange
|
||||
im = hopper("CMYK")
|
||||
|
|
|
@ -6,8 +6,8 @@ from .helper import hopper
|
|||
|
||||
|
||||
def test_copy():
|
||||
croppedCoordinates = (10, 10, 20, 20)
|
||||
croppedSize = (10, 10)
|
||||
cropped_coordinates = (10, 10, 20, 20)
|
||||
cropped_size = (10, 10)
|
||||
for mode in "1", "P", "L", "RGB", "I", "F":
|
||||
# Internal copy method
|
||||
im = hopper(mode)
|
||||
|
@ -23,15 +23,15 @@ def test_copy():
|
|||
|
||||
# Internal copy method on a cropped image
|
||||
im = hopper(mode)
|
||||
out = im.crop(croppedCoordinates).copy()
|
||||
out = im.crop(cropped_coordinates).copy()
|
||||
assert out.mode == im.mode
|
||||
assert out.size == croppedSize
|
||||
assert out.size == cropped_size
|
||||
|
||||
# Python's copy method on a cropped image
|
||||
im = hopper(mode)
|
||||
out = copy.copy(im.crop(croppedCoordinates))
|
||||
out = copy.copy(im.crop(cropped_coordinates))
|
||||
assert out.mode == im.mode
|
||||
assert out.size == croppedSize
|
||||
assert out.size == cropped_size
|
||||
|
||||
|
||||
def test_copy_zero():
|
||||
|
|
|
@ -99,10 +99,10 @@ def test_rankfilter_properties():
|
|||
|
||||
|
||||
def test_builtinfilter_p():
|
||||
builtinFilter = ImageFilter.BuiltinFilter()
|
||||
builtin_filter = ImageFilter.BuiltinFilter()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builtinFilter.filter(hopper("P"))
|
||||
builtin_filter.filter(hopper("P"))
|
||||
|
||||
|
||||
def test_kernel_not_enough_coefficients():
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageQt
|
||||
from PIL import Image
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_equal, cached_property
|
||||
from .helper import CachedProperty, assert_image_equal
|
||||
|
||||
|
||||
class TestImagingPaste:
|
||||
|
@ -34,7 +34,7 @@ class TestImagingPaste:
|
|||
im.paste(im2, mask)
|
||||
self.assert_9points_image(im, expected)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def mask_1(self):
|
||||
mask = Image.new("1", (self.size, self.size))
|
||||
px = mask.load()
|
||||
|
@ -43,11 +43,11 @@ class TestImagingPaste:
|
|||
px[y, x] = (x + y) % 2
|
||||
return mask
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def mask_L(self):
|
||||
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_L(self):
|
||||
gradient = Image.new("L", (self.size, self.size))
|
||||
px = gradient.load()
|
||||
|
@ -56,7 +56,7 @@ class TestImagingPaste:
|
|||
px[y, x] = (x + y) % 255
|
||||
return gradient
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_RGB(self):
|
||||
return Image.merge(
|
||||
"RGB",
|
||||
|
@ -67,7 +67,7 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_LA(self):
|
||||
return Image.merge(
|
||||
"LA",
|
||||
|
@ -77,7 +77,7 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_RGBA(self):
|
||||
return Image.merge(
|
||||
"RGBA",
|
||||
|
@ -89,7 +89,7 @@ class TestImagingPaste:
|
|||
],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@CachedProperty
|
||||
def gradient_RGBa(self):
|
||||
return Image.merge(
|
||||
"RGBa",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
|
||||
|
||||
|
@ -10,17 +12,31 @@ def test_sanity():
|
|||
im.point(list(range(256)))
|
||||
im.point(list(range(256)) * 3)
|
||||
im.point(lambda x: x)
|
||||
im.point(lambda x: x * 1.2)
|
||||
|
||||
im = im.convert("I")
|
||||
with pytest.raises(ValueError):
|
||||
im.point(list(range(256)))
|
||||
im.point(lambda x: x * 1)
|
||||
im.point(lambda x: x + 1)
|
||||
im.point(lambda x: x - 1)
|
||||
im.point(lambda x: x * 1 + 1)
|
||||
im.point(lambda x: 0.1 + 0.2 * x)
|
||||
im.point(lambda x: -x)
|
||||
im.point(lambda x: x - 0.5)
|
||||
im.point(lambda x: 1 - x / 2)
|
||||
im.point(lambda x: (2 + x) / 3)
|
||||
im.point(lambda x: 0.5)
|
||||
im.point(lambda x: x / 1)
|
||||
im.point(lambda x: x + x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: x - 1)
|
||||
im.point(lambda x: x * x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: x / 1)
|
||||
im.point(lambda x: x / x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: 1 / x)
|
||||
with pytest.raises(TypeError):
|
||||
im.point(lambda x: x // 2)
|
||||
|
||||
|
||||
def test_16bit_lut():
|
||||
|
@ -46,3 +62,8 @@ def test_f_mode():
|
|||
im = hopper("F")
|
||||
with pytest.raises(ValueError):
|
||||
im.point(None)
|
||||
|
||||
|
||||
def test_coerce_e_deprecation():
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert Image.coerce_e(2).data == 2
|
||||
|
|
|
@ -458,7 +458,7 @@ class TestCoreResampleBox:
|
|||
def split_range(size, tiles):
|
||||
scale = size / tiles
|
||||
for i in range(tiles):
|
||||
yield (int(round(scale * i)), int(round(scale * (i + 1))))
|
||||
yield int(round(scale * i)), int(round(scale * (i + 1)))
|
||||
|
||||
tiled = Image.new(im.mode, dst_size)
|
||||
scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1])
|
||||
|
|
|
@ -12,6 +12,7 @@ from .helper import (
|
|||
assert_image_equal_tofile,
|
||||
assert_image_similar,
|
||||
hopper,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
|
||||
|
@ -264,6 +265,14 @@ class TestImageResize:
|
|||
with pytest.raises(ValueError):
|
||||
im.resize((10, 10), "unknown")
|
||||
|
||||
@skip_unless_feature("libtiff")
|
||||
def test_load_first(self):
|
||||
# load() may change the size of the image
|
||||
# Test that resize() is calling it before getting the size
|
||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||
im = im.resize((64, 64))
|
||||
assert im.size == (64, 64)
|
||||
|
||||
def test_default_filter(self):
|
||||
for mode in "L", "RGB", "I", "F":
|
||||
im = hopper(mode)
|
||||
|
|
|
@ -7,6 +7,7 @@ from .helper import (
|
|||
assert_image_similar,
|
||||
fromstring,
|
||||
hopper,
|
||||
skip_unless_feature,
|
||||
tostring,
|
||||
)
|
||||
|
||||
|
@ -88,6 +89,7 @@ def test_no_resize():
|
|||
assert im.size == (64, 64)
|
||||
|
||||
|
||||
@skip_unless_feature("libtiff")
|
||||
def test_load_first():
|
||||
# load() may change the size of the image
|
||||
# Test that thumbnail() is calling it before performing size calculations
|
||||
|
|
|
@ -655,6 +655,20 @@ def test_polygon_1px_high():
|
|||
assert_image_equal_tofile(im, expected)
|
||||
|
||||
|
||||
def test_polygon_1px_high_translucent():
|
||||
# Test drawing a translucent 1px high polygon
|
||||
# Arrange
|
||||
im = Image.new("RGB", (4, 3))
|
||||
draw = ImageDraw.Draw(im, "RGBA")
|
||||
expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png"
|
||||
|
||||
# Act
|
||||
draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127))
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im, expected)
|
||||
|
||||
|
||||
def test_polygon_translucent():
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
|
@ -1452,3 +1466,11 @@ def test_discontiguous_corners_polygon():
|
|||
)
|
||||
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
||||
assert_image_similar_tofile(img, expected, 1)
|
||||
|
||||
|
||||
def test_polygon():
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
|
||||
expected = "Tests/images/imagedraw_outline_polygon_RGB.png"
|
||||
assert_image_similar_tofile(im, expected, 1)
|
||||
|
|
|
@ -2,7 +2,15 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features
|
||||
from PIL import (
|
||||
BmpImagePlugin,
|
||||
EpsImagePlugin,
|
||||
Image,
|
||||
ImageFile,
|
||||
UnidentifiedImageError,
|
||||
_binary,
|
||||
features,
|
||||
)
|
||||
|
||||
from .helper import (
|
||||
assert_image,
|
||||
|
@ -35,9 +43,9 @@ class TestImageFile:
|
|||
|
||||
parser = ImageFile.Parser()
|
||||
parser.feed(data)
|
||||
imOut = parser.close()
|
||||
im_out = parser.close()
|
||||
|
||||
return im, imOut
|
||||
return im, im_out
|
||||
|
||||
assert_image_equal(*roundtrip("BMP"))
|
||||
im1, im2 = roundtrip("GIF")
|
||||
|
@ -377,3 +385,7 @@ class TestPyEncoder(CodecsTest):
|
|||
|
||||
with pytest.raises(NotImplementedError):
|
||||
encoder.encode_to_file(None, None)
|
||||
|
||||
def test_zero_height(self):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
Image.open("Tests/images/zero_height.j2k")
|
||||
|
|
|
@ -65,9 +65,12 @@ class TestImageFont:
|
|||
return font_bytes
|
||||
|
||||
def test_font_with_filelike(self):
|
||||
ImageFont.truetype(
|
||||
ttf = ImageFont.truetype(
|
||||
self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
|
||||
)
|
||||
ttf_copy = ttf.font_variant()
|
||||
assert ttf_copy.font_bytes == ttf.font_bytes
|
||||
|
||||
self._render(self._font_as_bytes())
|
||||
# Usage note: making two fonts from the same buffer fails.
|
||||
# shared_bytes = self._font_as_bytes()
|
||||
|
@ -977,6 +980,14 @@ class TestImageFont:
|
|||
|
||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
|
||||
|
||||
def test_fill_deprecation(self):
|
||||
font = self.get_font()
|
||||
with pytest.warns(DeprecationWarning):
|
||||
font.getmask2("Hello world", fill=Image.core.fill)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.raises(TypeError):
|
||||
font.getmask2("Hello world", fill=None)
|
||||
|
||||
|
||||
@skip_unless_feature("raqm")
|
||||
class TestImageFont_RaqmLayout(TestImageFont):
|
||||
|
|
|
@ -48,12 +48,8 @@ def img_string_normalize(im):
|
|||
return img_to_string(string_to_img(im))
|
||||
|
||||
|
||||
def assert_img_equal(A, B):
|
||||
assert img_to_string(A) == img_to_string(B)
|
||||
|
||||
|
||||
def assert_img_equal_img_string(A, Bstring):
|
||||
assert img_to_string(A) == img_string_normalize(Bstring)
|
||||
def assert_img_equal_img_string(a, b_string):
|
||||
assert img_to_string(a) == img_string_normalize(b_string)
|
||||
|
||||
|
||||
def test_str_to_img():
|
||||
|
|
|
@ -174,7 +174,7 @@ def test_overflow_segfault():
|
|||
# through to the sequence. Seeing this on 32-bit Windows.
|
||||
with pytest.raises((TypeError, MemoryError)):
|
||||
# post patch, this fails with a memory error
|
||||
x = evil()
|
||||
x = Evil()
|
||||
|
||||
# This fails due to the invalid malloc above,
|
||||
# and segfaults
|
||||
|
@ -182,7 +182,7 @@ def test_overflow_segfault():
|
|||
x[i] = b"0" * 16
|
||||
|
||||
|
||||
class evil:
|
||||
class Evil:
|
||||
def __init__(self):
|
||||
self.corrupt = Image.core.path(0x4000000000000000)
|
||||
|
||||
|
|
|
@ -2,10 +2,13 @@ import warnings
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_similar, hopper
|
||||
|
||||
with warnings.catch_warnings() as w:
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
|
||||
)
|
||||
|
|
|
@ -65,21 +65,21 @@ def test_libtiff():
|
|||
|
||||
def test_consecutive():
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
firstFrame = None
|
||||
first_frame = None
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
if firstFrame is None:
|
||||
firstFrame = frame.copy()
|
||||
if first_frame is None:
|
||||
first_frame = frame.copy()
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
assert_image_equal(frame, firstFrame)
|
||||
assert_image_equal(frame, first_frame)
|
||||
break
|
||||
|
||||
|
||||
def test_palette_mmap():
|
||||
# Using mmap in ImageFile can require to reload the palette.
|
||||
with Image.open("Tests/images/multipage-mmap.tiff") as im:
|
||||
color1 = im.getpalette()[0:3]
|
||||
color1 = im.getpalette()[:3]
|
||||
im.seek(0)
|
||||
color2 = im.getpalette()[0:3]
|
||||
color2 = im.getpalette()[:3]
|
||||
assert color1 == color2
|
||||
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ def setup_module():
|
|||
# setup tk
|
||||
tk.Frame()
|
||||
# root = tk.Tk()
|
||||
except RuntimeError as v:
|
||||
pytest.skip(f"RuntimeError: {v}")
|
||||
except tk.TclError as v:
|
||||
pytest.skip(f"TCL Error: {v}")
|
||||
|
||||
|
|
|
@ -26,51 +26,51 @@ def test_basic(tmp_path):
|
|||
|
||||
def basic(mode):
|
||||
|
||||
imIn = original.convert(mode)
|
||||
verify(imIn)
|
||||
im_in = original.convert(mode)
|
||||
verify(im_in)
|
||||
|
||||
w, h = imIn.size
|
||||
w, h = im_in.size
|
||||
|
||||
imOut = imIn.copy()
|
||||
verify(imOut) # copy
|
||||
im_out = im_in.copy()
|
||||
verify(im_out) # copy
|
||||
|
||||
imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
|
||||
verify(imOut) # transform
|
||||
im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
|
||||
verify(im_out) # transform
|
||||
|
||||
filename = str(tmp_path / "temp.im")
|
||||
imIn.save(filename)
|
||||
im_in.save(filename)
|
||||
|
||||
with Image.open(filename) as imOut:
|
||||
with Image.open(filename) as im_out:
|
||||
|
||||
verify(imIn)
|
||||
verify(imOut)
|
||||
verify(im_in)
|
||||
verify(im_out)
|
||||
|
||||
imOut = imIn.crop((0, 0, w, h))
|
||||
verify(imOut)
|
||||
im_out = im_in.crop((0, 0, w, h))
|
||||
verify(im_out)
|
||||
|
||||
imOut = Image.new(mode, (w, h), None)
|
||||
imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0))
|
||||
imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0))
|
||||
im_out = Image.new(mode, (w, h), None)
|
||||
im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
|
||||
im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
|
||||
|
||||
verify(imIn)
|
||||
verify(imOut)
|
||||
verify(im_in)
|
||||
verify(im_out)
|
||||
|
||||
imIn = Image.new(mode, (1, 1), 1)
|
||||
assert imIn.getpixel((0, 0)) == 1
|
||||
im_in = Image.new(mode, (1, 1), 1)
|
||||
assert im_in.getpixel((0, 0)) == 1
|
||||
|
||||
imIn.putpixel((0, 0), 2)
|
||||
assert imIn.getpixel((0, 0)) == 2
|
||||
im_in.putpixel((0, 0), 2)
|
||||
assert im_in.getpixel((0, 0)) == 2
|
||||
|
||||
if mode == "L":
|
||||
maximum = 255
|
||||
else:
|
||||
maximum = 32767
|
||||
|
||||
imIn = Image.new(mode, (1, 1), 256)
|
||||
assert imIn.getpixel((0, 0)) == min(256, maximum)
|
||||
im_in = Image.new(mode, (1, 1), 256)
|
||||
assert im_in.getpixel((0, 0)) == min(256, maximum)
|
||||
|
||||
imIn.putpixel((0, 0), 512)
|
||||
assert imIn.getpixel((0, 0)) == min(512, maximum)
|
||||
im_in.putpixel((0, 0), 512)
|
||||
assert im_in.getpixel((0, 0)) == min(512, maximum)
|
||||
|
||||
basic("L")
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import ImageQt
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import ImageQt
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", category=DeprecationWarning)
|
||||
from PIL import ImageQt
|
||||
|
||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ def test_is_path():
|
|||
fp = "filename.ext"
|
||||
|
||||
# Act
|
||||
it_is = _util.isPath(fp)
|
||||
it_is = _util.is_path(fp)
|
||||
|
||||
# Assert
|
||||
assert it_is
|
||||
|
@ -21,7 +21,7 @@ def test_path_obj_is_path():
|
|||
test_path = Path("filename.ext")
|
||||
|
||||
# Act
|
||||
it_is = _util.isPath(test_path)
|
||||
it_is = _util.is_path(test_path)
|
||||
|
||||
# Assert
|
||||
assert it_is
|
||||
|
@ -33,7 +33,7 @@ def test_is_not_path(tmp_path):
|
|||
pass
|
||||
|
||||
# Act
|
||||
it_is_not = _util.isPath(fp)
|
||||
it_is_not = _util.is_path(fp)
|
||||
|
||||
# Assert
|
||||
assert not it_is_not
|
||||
|
@ -44,7 +44,7 @@ def test_is_directory():
|
|||
directory = "Tests"
|
||||
|
||||
# Act
|
||||
it_is = _util.isDirectory(directory)
|
||||
it_is = _util.is_directory(directory)
|
||||
|
||||
# Assert
|
||||
assert it_is
|
||||
|
@ -55,7 +55,7 @@ def test_is_not_directory():
|
|||
text = "abc"
|
||||
|
||||
# Act
|
||||
it_is_not = _util.isDirectory(text)
|
||||
it_is_not = _util.is_directory(text)
|
||||
|
||||
# Assert
|
||||
assert not it_is_not
|
||||
|
@ -65,7 +65,7 @@ def test_deferred_error():
|
|||
# Arrange
|
||||
|
||||
# Act
|
||||
thing = _util.deferred_error(ValueError("Some error text"))
|
||||
thing = _util.DeferredError(ValueError("Some error text"))
|
||||
|
||||
# Assert
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install openjpeg
|
||||
|
||||
archive=openjpeg-2.4.0
|
||||
archive=openjpeg-2.5.0
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ release = PIL.__version__
|
|||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = "en"
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
|
|
|
@ -97,8 +97,8 @@ Deprecated Use instead
|
|||
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
|
||||
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
|
||||
``Image.BOX`` ``Image.Resampling.BOX``
|
||||
``Image.BILINEAR`` ``Image.Resampling.BILNEAR``
|
||||
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
|
||||
``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
|
||||
``Image.LINEAR`` ``Image.Resampling.BILINEAR``
|
||||
``Image.HAMMING`` ``Image.Resampling.HAMMING``
|
||||
``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
|
||||
``Image.CUBIC`` ``Image.Resampling.BICUBIC``
|
||||
|
@ -142,6 +142,14 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
|
|||
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
|
||||
:mod:`~PIL.FitsImagePlugin` instead.
|
||||
|
||||
FreeTypeFont.getmask2 fill parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
|
||||
deprecated and will be removed in Pillow 10 (2023-07-01).
|
||||
|
||||
PhotoImage.paste box parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -149,6 +157,27 @@ PhotoImage.paste box parameter
|
|||
|
||||
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
|
||||
|
||||
PyQt5 and PySide2
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
|
||||
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
|
||||
|
||||
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
|
||||
in Pillow 10 (2023-07-01). Upgrade to
|
||||
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
|
||||
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
|
||||
|
||||
Image.coerce_e
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
This undocumented method has been deprecated and will be removed in Pillow 10
|
||||
(2023-07-01).
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@ When an image is opened from a file, only that instance of the image is consider
|
|||
have the format. Copies of the image will contain data loaded from the file, but not
|
||||
the file itself, meaning that it can no longer be considered to be in the original
|
||||
format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
|
||||
internally creates a copy of the image, the ``fp`` (file pointer), along with any
|
||||
methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format`
|
||||
attribute will be ``None``.
|
||||
internally creates a copy of the image, then any methods or attributes specific to the
|
||||
format will no longer be present. The ``fp`` (file pointer) attribute will no longer be
|
||||
present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
|
||||
|
||||
Fully supported formats
|
||||
-----------------------
|
||||
|
@ -101,8 +101,8 @@ GIF
|
|||
^^^
|
||||
|
||||
Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
|
||||
writes run-length encoded files in GIF87a by default, unless GIF89a features
|
||||
are used or GIF89a is already in use.
|
||||
writes files in GIF87a by default, unless GIF89a features are used or GIF89a is
|
||||
already in use. Files are written with LZW encoding.
|
||||
|
||||
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
|
||||
images. Seeking to later frames in a ``P`` image will change the image to
|
||||
|
@ -156,7 +156,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
|
|||
it will loop forever.
|
||||
|
||||
**comment**
|
||||
May not be present. A comment about the image.
|
||||
May not be present. A comment about the image. This is the last comment found
|
||||
before the current frame's image.
|
||||
|
||||
**extension**
|
||||
May not be present. Contains application specific information.
|
||||
|
@ -245,17 +246,14 @@ Reading local images
|
|||
|
||||
The GIF loader creates an image memory the same size as the GIF file’s *logical
|
||||
screen size*, and pastes the actual pixel data (the *local image*) into this
|
||||
image. If you only want the actual pixel rectangle, you can manipulate the
|
||||
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.ImageFile.ImageFile.tile`
|
||||
attributes before loading the file::
|
||||
image. If you only want the actual pixel rectangle, you can crop the image::
|
||||
|
||||
im = Image.open(...)
|
||||
|
||||
if im.tile[0][0] == "gif":
|
||||
# only read the first "local image" from this GIF file
|
||||
tag, (x0, y0, x1, y1), offset, extra = im.tile[0]
|
||||
im.size = (x1 - x0, y1 - y0)
|
||||
im.tile = [(tag, (0, 0) + im.size, offset, extra)]
|
||||
box = im.tile[0][1]
|
||||
im = im.crop(box)
|
||||
|
||||
ICNS
|
||||
^^^^
|
||||
|
@ -1235,6 +1233,11 @@ PSD
|
|||
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
|
||||
|
||||
|
||||
SUN
|
||||
^^^
|
||||
|
||||
Pillow identifies and reads Sun raster files.
|
||||
|
||||
WAL
|
||||
^^^
|
||||
|
||||
|
@ -1249,13 +1252,13 @@ this format.
|
|||
By default, a Quake2 standard palette is attached to the texture. To override
|
||||
the palette, use the putpalette method.
|
||||
|
||||
WMF
|
||||
^^^
|
||||
WMF, EMF
|
||||
^^^^^^^^
|
||||
|
||||
Pillow can identify WMF files.
|
||||
Pillow can identify WMF and EMF files.
|
||||
|
||||
On Windows, it can read WMF files. By default, it will load the image at 72
|
||||
dpi. To load it at another resolution:
|
||||
On Windows, it can read WMF and EMF files. By default, it will load the image
|
||||
at 72 dpi. To load it at another resolution:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -1265,7 +1268,8 @@ dpi. To load it at another resolution:
|
|||
im.load(dpi=144)
|
||||
|
||||
To add other read or write support, use
|
||||
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.
|
||||
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
|
||||
handler.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
|
@ -504,6 +504,17 @@ image header. In addition, seek will also be used when the image data is read
|
|||
tar file, you can use the :py:class:`~PIL.ContainerIO` or
|
||||
:py:class:`~PIL.TarIO` modules to access it.
|
||||
|
||||
Reading from URL
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
from PIL import Image
|
||||
from urllib.request import urlopen
|
||||
url = "https://python-pillow.org/images/pillow-logo.png"
|
||||
img = Image.open(urlopen(url))
|
||||
|
||||
|
||||
Reading from a tar archive
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -29,6 +29,10 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
|
|||
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
|
||||
:alt: GitHub Actions build status (Test MinGW)
|
||||
|
||||
.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg
|
||||
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
|
||||
:alt: GitHub Actions build status (Test Cygwin)
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build
|
||||
:target: https://ci.appveyor.com/project/python-pillow/Pillow
|
||||
:alt: AppVeyor CI build status (Windows)
|
||||
|
|
|
@ -162,7 +162,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libtiff** provides compressed TIFF functionality
|
||||
|
||||
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.3**
|
||||
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.4**
|
||||
|
||||
* **libfreetype** provides type related services
|
||||
|
||||
|
@ -181,7 +181,8 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **openjpeg** provides JPEG 2000 functionality.
|
||||
|
||||
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1** and **2.4.0**.
|
||||
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
|
||||
**2.4.0** and **2.5.0**.
|
||||
* Pillow does **not** support the earlier **1.5** series which ships
|
||||
with Debian Jessie.
|
||||
|
||||
|
@ -463,22 +464,29 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 35 | 3.10 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 36 | 3.10 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Gentoo | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
|
||||
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
|
||||
| | PyPy3 | |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.8 | arm64v8, ppc64le, |
|
||||
| | | s390x |
|
||||
| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
|
||||
| | PyPy3 | |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, |
|
||||
| | | s390x, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2016 | 3.7 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 |
|
||||
| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 |
|
||||
| | PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.9/MinGW | x86, x86-64 |
|
||||
| | 3.9 (MinGW) | x86, x86-64 |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
|
||||
|
||||
|
@ -496,13 +504,13 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+===========================+==================+==============+
|
||||
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |arm |
|
||||
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |arm |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
|
||||
| +---------------------------+------------------+--------------+
|
||||
| | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |x86-64 |
|
||||
| +---------------------------+------------------+--------------+
|
||||
| | 3.6 | 8.4.0 |x86-64 |
|
||||
| | 3.7, 3.8, 3.9, 3.10 | 9.1.1 |x86-64 |
|
||||
| +---------------------------+------------------+ |
|
||||
| | 3.6 | 8.4.0 | |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
|
||||
| +---------------------------+------------------+ |
|
||||
|
@ -563,6 +571,8 @@ These platforms have been reported to work at the versions mentioned.
|
|||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| Windows 10 | 3.7 | 7.1.0 |x86-64 |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
|
||||
|
|
|
@ -123,6 +123,7 @@ methods. Unless otherwise stated, all methods return a new instance of the
|
|||
|
||||
|
||||
.. automethod:: PIL.Image.Image.alpha_composite
|
||||
.. automethod:: PIL.Image.Image.apply_transparency
|
||||
.. automethod:: PIL.Image.Image.convert
|
||||
|
||||
The following example converts an RGB image (linearly calibrated according to
|
||||
|
|
|
@ -7,6 +7,14 @@
|
|||
The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5
|
||||
or PySide2 QImage objects from PIL images.
|
||||
|
||||
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
|
||||
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
|
||||
|
||||
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
|
||||
in Pillow 10 (2023-07-01). Upgrade to
|
||||
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
|
||||
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
|
||||
|
||||
.. versionadded:: 1.1.6
|
||||
|
||||
.. py:class:: ImageQt(image)
|
||||
|
|
|
@ -25,3 +25,8 @@ The :py:class:`~PIL.ImageSequence.Iterator` class
|
|||
|
||||
.. autoclass:: PIL.ImageSequence.Iterator
|
||||
:members:
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
.. autofunction:: PIL.ImageSequence.all_frames
|
||||
|
|
|
@ -10,6 +10,10 @@ metadata tag numbers, names, and type information.
|
|||
.. method:: lookup(tag)
|
||||
|
||||
:param tag: Integer tag number
|
||||
:param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in
|
||||
|
||||
.. versionadded:: 8.3.0
|
||||
|
||||
:returns: Taginfo namedtuple, From the :py:data:`~PIL.TiffTags.TAGS_V2` info if possible,
|
||||
otherwise just populating the value and name from :py:data:`~PIL.TiffTags.TAGS`.
|
||||
If the tag is not recognized, "unknown" is returned for the name
|
||||
|
@ -42,6 +46,16 @@ metadata tag numbers, names, and type information.
|
|||
|
||||
.. versionadded:: 3.0.0
|
||||
|
||||
.. py:data:: PIL.TiffTags.TAGS_V2_GROUPS
|
||||
:type: dict
|
||||
|
||||
:py:data:`~PIL.TiffTags.TAGS_V2` is one dimensional and
|
||||
doesn't account for the fact that tags actually exist in
|
||||
`different groups <https://exiftool.org/TagNames/EXIF.html>`_.
|
||||
This dictionary is used when the tag in question is part of a group.
|
||||
|
||||
.. versionadded:: 8.3.0
|
||||
|
||||
.. py:data:: PIL.TiffTags.TAGS
|
||||
:type: dict
|
||||
|
||||
|
|
|
@ -9,6 +9,14 @@ Internal Modules
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL._deprecate` Module
|
||||
-----------------------------
|
||||
|
||||
.. automodule:: PIL._deprecate
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`~PIL._tkinter_finder` Module
|
||||
----------------------------------
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ Previously, if a BMP file was too large, an ``OSError`` would be raised. Now,
|
|||
Dark theme for docs
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The https://pillow.readthedocs.io documentation will use a dark theme if the the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
|
||||
The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -149,6 +149,9 @@ Switched to libjpeg-turbo in macOS and Linux wheels
|
|||
The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to
|
||||
libjpeg-turbo. It is a fork of libjpeg, popular for its speed.
|
||||
|
||||
Because different JPEG decoders load images differently, JPEG pixels may be
|
||||
altered slightly with this change.
|
||||
|
||||
Added support for pickling TrueType fonts
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -76,8 +76,8 @@ Deprecated Use instead
|
|||
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
|
||||
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
|
||||
``Image.BOX`` ``Image.Resampling.BOX``
|
||||
``Image.BILINEAR`` ``Image.Resampling.BILNEAR``
|
||||
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
|
||||
``Image.BILINEAR`` ``Image.Resampling.BILINEAR``
|
||||
``Image.LINEAR`` ``Image.Resampling.BILINEAR``
|
||||
``Image.HAMMING`` ``Image.Resampling.HAMMING``
|
||||
``Image.BICUBIC`` ``Image.Resampling.BICUBIC``
|
||||
``Image.CUBIC`` ``Image.Resampling.BICUBIC``
|
||||
|
|
16
docs/releasenotes/9.1.1.rst
Normal file
|
@ -0,0 +1,16 @@
|
|||
9.1.1
|
||||
-----
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
This release addresses several security problems.
|
||||
|
||||
:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines,
|
||||
Pillow reads the information past the end of the first line without deducting that
|
||||
from the length of the remaining file data. This vulnerability was introduced in Pillow
|
||||
9.1.0, and can cause a heap buffer overflow.
|
||||
|
||||
Opening an image with a zero or negative height has been found to bypass a
|
||||
decompression bomb check. This will now raise a :py:exc:`SyntaxError` instead, in turn
|
||||
raising a ``PIL.UnidentifiedImageError``.
|
82
docs/releasenotes/9.2.0.rst
Normal file
|
@ -0,0 +1,82 @@
|
|||
9.2.0
|
||||
-----
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
PyQt5 and PySide2
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
|
||||
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
|
||||
|
||||
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
|
||||
in Pillow 10 (2023-07-01). Upgrade to
|
||||
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
|
||||
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
|
||||
|
||||
FreeTypeFont.getmask2 fill parameter
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2`
|
||||
has been deprecated and will be removed in Pillow 10 (2023-07-01).
|
||||
|
||||
PhotoImage.paste box parameter
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
|
||||
|
||||
Image.coerce_e
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 9.2.0
|
||||
|
||||
This undocumented method has been deprecated and will be removed in Pillow 10
|
||||
(2023-07-01).
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
Image.apply_transparency
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Added :py:meth:`~PIL.Image.Image.apply_transparency`, a method to take a P mode image
|
||||
with "transparency" in ``im.info``, and apply the transparency to the palette instead.
|
||||
The image's palette mode will become "RGBA", and "transparency" will be removed from
|
||||
``im.info``.
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|