Merge branch 'python-pillow'

This commit is contained in:
Andrew Murray 2023-02-25 21:15:30 +11:00
commit 23f55505c8
174 changed files with 1260 additions and 895 deletions

View File

@ -21,9 +21,11 @@ environment:
install: install:
- '%PYTHON%\%EXECUTABLE% --version' - '%PYTHON%\%EXECUTABLE% --version'
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-depends.zip -oc:\ - 7z x pillow-depends.zip -oc:\
- 7z x pillow-test-images.zip -oc:\
- mv c:\pillow-depends-main c:\pillow-depends - mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
- ..\pillow-depends\gs1000w32.exe /S - ..\pillow-depends\gs1000w32.exe /S
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH%

View File

@ -37,7 +37,8 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy # TODO Remove condition when NumPy supports 3.12
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

View File

@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install numpy # TODO Remove condition when NumPy supports 3.12
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -34,18 +34,34 @@ jobs:
with: with:
platform: x86_64 platform: x86_64
packages: > packages: >
ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel gcc-g++
libimagequant-devel libjpeg-devel liblapack-devel ghostscript
liblcms2-devel libopenjp2-devel libraqm-devel ImageMagick
libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 jpeg
make netpbm perl 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 }}-cffi
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter python3${{ matrix.python-minor-version }}-tkinter
qt5-devel-tools subversion xorg-server-extra zlib-devel qt5-devel-tools
wget
xorg-server-extra
zlib-devel
- name: Add Lapack to PATH - name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v3 uses: egor-tensin/cleanup-path@v3

View File

@ -87,6 +87,7 @@ jobs:
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
gcov: true
success: success:
permissions: permissions:

View File

@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -45,12 +45,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
pacman -S --noconfirm \ pacman -S --noconfirm \
${{ matrix.package }}-python3-cffi \
${{ matrix.package }}-python3-numpy \
${{ matrix.package }}-python3-olefile \
${{ matrix.package }}-python3-pip \
${{ matrix.package }}-python-pyqt6 \
${{ matrix.package }}-python3-setuptools \
${{ matrix.package }}-freetype \ ${{ matrix.package }}-freetype \
${{ matrix.package }}-gcc \ ${{ matrix.package }}-gcc \
${{ matrix.package }}-ghostscript \ ${{ matrix.package }}-ghostscript \
@ -61,7 +55,16 @@ jobs:
${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libtiff \
${{ matrix.package }}-libwebp \ ${{ matrix.package }}-libwebp \
${{ matrix.package }}-openjpeg2 \ ${{ matrix.package }}-openjpeg2 \
subversion ${{ matrix.package }}-python3-cffi \
${{ matrix.package }}-python3-numpy \
${{ matrix.package }}-python3-olefile \
${{ matrix.package }}-python3-pip \
${{ matrix.package }}-python3-setuptools
if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then
pacman -S --noconfirm \
${{ matrix.package }}-python-pyqt6
fi
python3 -m pip install pyroma pytest pytest-cov pytest-timeout python3 -m pip install pyroma pytest pytest-cov pytest-timeout

View File

@ -48,5 +48,5 @@ jobs:
run: | run: |
# The Pillow user in the docker container is UID 1000 # The Pillow user in the docker container is UID 1000
sudo chown -R 1000 $GITHUB_WORKSPACE sudo chown -R 1000 $GITHUB_WORKSPACE
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE sudo chown -R runner $GITHUB_WORKSPACE

View File

@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
architecture: ["x86", "x64"] architecture: ["x86", "x64"]
include: include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows # PyPy 7.3.4+ only ships 64-bit binaries for Windows
@ -38,6 +38,12 @@ jobs:
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images
uses: actions/checkout@v3
with:
repository: python-pillow/test-images
path: Tests\test-images
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
@ -62,7 +68,8 @@ jobs:
winbuild\depends\gs1000w32.exe /S winbuild\depends\gs1000w32.exe /S
echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\ # Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
# make cache key depend on VS version # make cache key depend on VS version
& "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" `

View File

@ -22,6 +22,7 @@ jobs:
python-version: [ python-version: [
"pypy3.9", "pypy3.9",
"pypy3.8", "pypy3.8",
"3.12-dev",
"3.11", "3.11",
"3.10", "3.10",
"3.9", "3.9",
@ -107,9 +108,9 @@ jobs:
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
file: ./coverage.xml
flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true
success: success:
permissions: permissions:

2
.gitignore vendored
View File

@ -79,7 +79,7 @@ docs/_build/
# JetBrains # JetBrains
.idea .idea
# Extra test images installed from pillow-depends/test_images # Extra test images installed from python-pillow/test-images
Tests/images/README.md Tests/images/README.md
Tests/images/crash_1.tif Tests/images/crash_1.tif
Tests/images/crash_2.tif Tests/images/crash_2.tif

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.12.0 rev: 23.1.0
hooks: hooks:
- id: black - id: black
args: [--target-version=py37] args: [--target-version=py37]
@ -9,7 +9,7 @@ repos:
types: [] types: []
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.11.1 rev: 5.12.0
hooks: hooks:
- id: isort - id: isort
@ -26,7 +26,7 @@ repos:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.3.1 rev: v1.4.2
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
@ -39,7 +39,7 @@ repos:
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat] [flake8-2020, flake8-errmsg, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0 rev: v1.10.0
hooks: hooks:
- id: python-check-blanket-noqa - id: python-check-blanket-noqa
- id: rst-backticks - id: rst-backticks
@ -57,7 +57,7 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/tox-ini-fmt - repo: https://github.com/tox-dev/tox-ini-fmt
rev: 0.5.2 rev: 0.6.1
hooks: hooks:
- id: tox-ini-fmt - id: tox-ini-fmt

View File

@ -2,9 +2,60 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
9.4.0 (unreleased) 9.5.0 (unreleased)
------------------ ------------------
- Added memoryview support to frombytes() #6974
[radarhere]
- Allow comments in FITS images #6973
[radarhere]
- Support saving PDF with different X and Y resolutions #6961
[jvanderneutstulen, radarhere, hugovk]
- Fixed writing int as UNDEFINED tag #6950
[radarhere]
- Raise an error if EXIF data is too long when saving JPEG #6939
[radarhere]
- Handle more than one directory returned by pkg-config #6896
[sebastic, radarhere]
- Do not retry past formats when loading all formats for the first time #6902
[radarhere]
- Do not retry specified formats if they failed when opening #6893
[radarhere]
- Do not unintentionally load TIFF format at first #6892
[radarhere]
- Stop reading when EPS line becomes too long #6897
[radarhere]
- Allow writing IFDRational to BYTE tag #6890
[radarhere]
- Raise ValueError for BoxBlur filter with negative radius #6874
[hugovk, radarhere]
- Support arbitrary number of loaded modules on Windows #6761
[javidcf, radarhere, nulano]
9.4.0 (2023-01-02)
------------------
- Fixed null pointer dereference crash with malformed font #6846
[wiredfool, radarhere]
- Return from ImagingFill early if image has a zero dimension #6842
[radarhere]
- Reversed deprecations for Image constants, except for duplicate Resampling attributes #6830
[radarhere]
- Improve exception traceback readability #6836 - Improve exception traceback readability #6836
[hugovk, radarhere] [hugovk, radarhere]

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is Pillow is the friendly PIL fork. It is
Copyright © 2010-2022 by Alex Clark and contributors Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License: Like PIL, Pillow is licensed under the open source HPND License:
@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated
documentation, you agree that you have read, understood, and will comply documentation, you agree that you have read, understood, and will comply
with the following terms and conditions: with the following terms and conditions:
Permission to use, copy, modify, and distribute this software and its Permission to use, copy, modify and distribute this software and its
associated documentation for any purpose and without fee is hereby granted, documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appears in all copies, and that provided that the above copyright notice appears in all copies, and that
both that copyright notice and this permission notice appear in supporting both that copyright notice and this permission notice appear in supporting
documentation, and that the name of Secret Labs AB or the author not be documentation, and that the name of Secret Labs AB or the author not be

View File

@ -6,8 +6,8 @@
## Python Imaging Library (Fork) ## Python Imaging Library (Fork)
Pillow is the friendly PIL fork by [Alex Clark and Pillow is the friendly PIL fork by [Jeffrey A. Clark (Alex) and
Contributors](https://github.com/python-pillow/Pillow/graphs/contributors). contributors](https://github.com/python-pillow/Pillow/graphs/contributors).
PIL is the Python Imaging Library by Fredrik Lundh and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
As of 2019, Pillow development is As of 2019, Pillow development is
[supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise). [supported by Tidelift](https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=readme&utm_campaign=enterprise).
@ -88,6 +88,10 @@ As of 2019, Pillow development is
<a href="https://twitter.com/PythonPillow"><img <a href="https://twitter.com/PythonPillow"><img
alt="Follow on https://twitter.com/PythonPillow" alt="Follow on https://twitter.com/PythonPillow"
src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a> src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a>
<a href="https://fosstodon.org/@pillow"><img
alt="Follow on https://fosstodon.org/@pillow"
src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg"
rel="me"></a>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -111,7 +111,7 @@ Released as needed privately to individual vendors for critical security-related
## Publicize Release ## Publicize Release
* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 * [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010
## Documentation ## Documentation

View File

@ -4,7 +4,6 @@ TEST_FILE = "Tests/images/fli_overflow.fli"
def test_fli_overflow(): def test_fli_overflow():
# this should not crash with a malloc error or access violation # this should not crash with a malloc error or access violation
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()

View File

@ -23,7 +23,6 @@ def test_ignore_dos_text():
def test_dos_text(): def test_dos_text():
try: try:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()

View File

@ -0,0 +1,10 @@
STARTFONT
FONT ÿ
SIZE 10
FONTBOUNDINGBOX
CHARS
STARTCHAR
ENCODING
BBX 2 5
ENDCHAR
ENDFONT

View File

@ -57,6 +57,6 @@ def test_fuzz_fonts(path):
with open(path, "rb") as f: with open(path, "rb") as f:
try: try:
fuzzers.fuzz_font(f.read()) fuzzers.fuzz_font(f.read())
except (Image.DecompressionBombError, Image.DecompressionBombWarning): except (Image.DecompressionBombError, Image.DecompressionBombWarning, OSError):
pass pass
assert True assert True

View File

@ -18,7 +18,6 @@ def test_bad():
"""These shouldn't crash/dos, but they shouldn't return anything """These shouldn't crash/dos, but they shouldn't return anything
either""" either"""
for f in get_files("b"): for f in get_files("b"):
# Assert that there is no unclosed file warning # Assert that there is no unclosed file warning
with warnings.catch_warnings(): with warnings.catch_warnings():
try: try:

View File

@ -11,6 +11,11 @@ from PIL import _deprecate
"Old thing is deprecated and will be removed in Pillow 10 " "Old thing is deprecated and will be removed in Pillow 10 "
r"\(2023-07-01\)\. Use new thing instead\.", r"\(2023-07-01\)\. Use new thing instead\.",
), ),
(
11,
"Old thing is deprecated and will be removed in Pillow 11 "
r"\(2024-10-15\)\. Use new thing instead\.",
),
( (
None, None,
r"Old thing is deprecated and will be removed in a future version\. " r"Old thing is deprecated and will be removed in a future version\. "

View File

@ -141,7 +141,6 @@ def test_rgba_bitfields():
# This test image has been manually hexedited # This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to RGBA # to change the bitfield compression in the header from XBGR to RGBA
with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
# So before the comparing the image, swap the channels # So before the comparing the image, swap the channels
b, g, r = im.split()[1:] b, g, r = im.split()[1:]
im = Image.merge("RGB", (r, g, b)) im = Image.merge("RGB", (r, g, b))

View File

@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d"
def test_open(): def test_open():
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
assert im.format == "BUFR" assert im.format == "BUFR"
@ -31,7 +30,6 @@ def test_invalid_file():
def test_load(): def test_load():
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler # Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()

View File

@ -15,7 +15,6 @@ def test_sanity():
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
assert im.size == (128, 128) assert im.size == (128, 128)
assert isinstance(im, DcxImagePlugin.DcxImageFile) assert isinstance(im, DcxImagePlugin.DcxImageFile)
@ -54,7 +53,6 @@ def test_invalid_file():
def test_tell(): def test_tell():
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act # Act
frame = im.tell() frame = im.tell()

View File

@ -80,7 +80,6 @@ def test_invalid_file():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_cmyk(): def test_cmyk():
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
assert cmyk_image.mode == "CMYK" assert cmyk_image.mode == "CMYK"
assert cmyk_image.size == (100, 100) assert cmyk_image.size == (100, 100)
assert cmyk_image.format == "EPS" assert cmyk_image.format == "EPS"

View File

@ -12,7 +12,6 @@ TEST_FILE = "Tests/images/hopper.fits"
def test_open(): def test_open():
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
assert im.format == "FITS" assert im.format == "FITS"
assert im.size == (128, 128) assert im.size == (128, 128)
@ -45,6 +44,12 @@ def test_naxis_zero():
pass pass
def test_comment():
image_data = b"SIMPLE = T / comment string"
with pytest.raises(OSError):
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
def test_stub_deprecated(): def test_stub_deprecated():
class Handler: class Handler:
opened = False opened = False

View File

@ -64,7 +64,6 @@ def test_context_manager():
def test_tell(): def test_tell():
# Arrange # Arrange
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
# Act # Act
frame = im.tell() frame = im.tell()
@ -110,7 +109,6 @@ def test_eoferror():
def test_seek_tell(): def test_seek_tell():
with Image.open(animated_test_file) as im: with Image.open(animated_test_file) as im:
layer_number = im.tell() layer_number = im.tell()
assert layer_number == 0 assert layer_number == 0

View File

@ -158,39 +158,42 @@ def test_optimize():
assert test_bilevel(1) == 799 assert test_bilevel(1) == 799
def test_optimize_correctness(): @pytest.mark.parametrize(
# 256 color Palette image, posterize to > 128 and < 128 levels "colors, size, expected_palette_length",
# Size bigger and smaller than 512x512 (
# These do optimize the palette
(256, 511, 256),
(255, 511, 255),
(129, 511, 129),
(128, 511, 128),
(64, 511, 64),
(4, 511, 4),
# These don't optimize the palette
(128, 513, 256),
(64, 513, 256),
(4, 513, 256),
),
)
def test_optimize_correctness(colors, size, expected_palette_length):
# 256 color Palette image, posterize to > 128 and < 128 levels.
# Size bigger and smaller than 512x512.
# Check the palette for number of colors allocated. # Check the palette for number of colors allocated.
# Check for correctness after conversion back to RGB # Check for correctness after conversion back to RGB.
def check(colors, size, expected_palette_length):
# make an image with empty colors in the start of the palette range
im = Image.frombytes(
"P", (colors, colors), bytes(range(256 - colors, 256)) * colors
)
im = im.resize((size, size))
outfile = BytesIO()
im.save(outfile, "GIF")
outfile.seek(0)
with Image.open(outfile) as reloaded:
# check palette length
palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v)
assert expected_palette_length == palette_length
assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) # make an image with empty colors in the start of the palette range
im = Image.frombytes(
"P", (colors, colors), bytes(range(256 - colors, 256)) * colors
)
im = im.resize((size, size))
outfile = BytesIO()
im.save(outfile, "GIF")
outfile.seek(0)
with Image.open(outfile) as reloaded:
# check palette length
palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v)
assert expected_palette_length == palette_length
# These do optimize the palette assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
check(256, 511, 256)
check(255, 511, 255)
check(129, 511, 129)
check(128, 511, 128)
check(64, 511, 64)
check(4, 511, 4)
# These don't optimize the palette
check(128, 513, 256)
check(64, 513, 256)
check(4, 513, 256)
def test_optimize_full_l(): def test_optimize_full_l():
@ -206,7 +209,7 @@ def test_optimize_if_palette_can_be_reduced_by_half():
im = im.resize((591, 443)) im = im.resize((591, 443))
im_rgb = im.convert("RGB") im_rgb = im.convert("RGB")
for (optimize, colors) in ((False, 256), (True, 8)): for optimize, colors in ((False, 256), (True, 8)):
out = BytesIO() out = BytesIO()
im_rgb.save(out, "GIF", optimize=optimize) im_rgb.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
@ -218,7 +221,6 @@ def test_roundtrip(tmp_path):
im = hopper() im = hopper()
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert_image_similar(reread.convert("RGB"), im, 50) assert_image_similar(reread.convert("RGB"), im, 50)
@ -229,7 +231,6 @@ def test_roundtrip2(tmp_path):
im2 = im.copy() im2 = im.copy()
im2.save(out) im2.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert_image_similar(reread.convert("RGB"), hopper(), 50) assert_image_similar(reread.convert("RGB"), hopper(), 50)
@ -239,7 +240,6 @@ def test_roundtrip_save_all(tmp_path):
im = hopper() im = hopper()
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
assert_image_similar(reread.convert("RGB"), im, 50) assert_image_similar(reread.convert("RGB"), im, 50)
# Multiframe image # Multiframe image
@ -281,13 +281,11 @@ def test_headers_saving_for_animated_gifs(tmp_path):
important_headers = ["background", "version", "duration", "loop"] important_headers = ["background", "version", "duration", "loop"]
# Multiframe image # Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
info = im.info.copy() info = im.info.copy()
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im.save(out, save_all=True) im.save(out, save_all=True)
with Image.open(out) as reread: with Image.open(out) as reread:
for header in important_headers: for header in important_headers:
assert info[header] == reread.info[header] assert info[header] == reread.info[header]
@ -305,7 +303,6 @@ def test_palette_handling(tmp_path):
im2.save(f, optimize=True) im2.save(f, optimize=True)
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
assert_image_similar(im, reloaded.convert("RGB"), 10) assert_image_similar(im, reloaded.convert("RGB"), 10)
@ -321,7 +318,6 @@ def test_palette_434(tmp_path):
orig = "Tests/images/test.colors.gif" orig = "Tests/images/test.colors.gif"
with Image.open(orig) as im: with Image.open(orig) as im:
with roundtrip(im) as reloaded: with roundtrip(im) as reloaded:
assert_image_similar(im, reloaded, 1) assert_image_similar(im, reloaded, 1)
with roundtrip(im, optimize=True) as reloaded: with roundtrip(im, optimize=True) as reloaded:
@ -572,7 +568,6 @@ def test_save_dispose(tmp_path):
) )
with Image.open(out) as img: with Image.open(out) as img:
for i in range(2): for i in range(2):
img.seek(img.tell() + 1) img.seek(img.tell() + 1)
assert img.disposal_method == i + 1 assert img.disposal_method == i + 1
@ -770,7 +765,6 @@ def test_multiple_duration(tmp_path):
out, save_all=True, append_images=im_list[1:], duration=duration_list out, save_all=True, append_images=im_list[1:], duration=duration_list
) )
with Image.open(out) as reread: with Image.open(out) as reread:
for duration in duration_list: for duration in duration_list:
assert reread.info["duration"] == duration assert reread.info["duration"] == duration
try: try:
@ -783,7 +777,6 @@ def test_multiple_duration(tmp_path):
out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list)
) )
with Image.open(out) as reread: with Image.open(out) as reread:
for duration in duration_list: for duration in duration_list:
assert reread.info["duration"] == duration assert reread.info["duration"] == duration
try: try:
@ -841,7 +834,6 @@ def test_identical_frames(tmp_path):
out, save_all=True, append_images=im_list[1:], duration=duration_list out, save_all=True, append_images=im_list[1:], duration=duration_list
) )
with Image.open(out) as reread: with Image.open(out) as reread:
# Assert that the first three frames were combined # Assert that the first three frames were combined
assert reread.n_frames == 2 assert reread.n_frames == 2

View File

@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb"
def test_open(): def test_open():
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
assert im.format == "GRIB" assert im.format == "GRIB"
@ -31,7 +30,6 @@ def test_invalid_file():
def test_load(): def test_load():
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler # Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()

View File

@ -8,7 +8,6 @@ TEST_FILE = "Tests/images/hdf5.h5"
def test_open(): def test_open():
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
assert im.format == "HDF5" assert im.format == "HDF5"
@ -29,7 +28,6 @@ def test_invalid_file():
def test_load(): def test_load():
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler # Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError): with pytest.raises(OSError):
im.load() im.load()

View File

@ -16,7 +16,6 @@ def test_sanity():
# Loading this icon by default should result in the largest size # Loading this icon by default should result in the largest size
# (512x512@2x) being loaded # (512x512@2x) being loaded
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert that there is no unclosed file warning # Assert that there is no unclosed file warning
with warnings.catch_warnings(): with warnings.catch_warnings():
im.load() im.load()

View File

@ -175,7 +175,6 @@ def test_save_256x256(tmp_path):
# Act # Act
im.save(outfile) im.save(outfile)
with Image.open(outfile) as im_saved: with Image.open(outfile) as im_saved:
# Assert # Assert
assert im_saved.size == (256, 256) assert im_saved.size == (256, 256)

View File

@ -51,7 +51,6 @@ def test_context_manager():
def test_tell(): def test_tell():
# Arrange # Arrange
with Image.open(TEST_IM) as im: with Image.open(TEST_IM) as im:
# Act # Act
frame = im.tell() frame = im.tell()

View File

@ -11,7 +11,6 @@ TEST_FILE = "Tests/images/iptc.jpg"
def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_none():
# Arrange # Arrange
with hopper() as im: with hopper() as im:
# Act # Act
iptc = IptcImagePlugin.getiptcinfo(im) iptc = IptcImagePlugin.getiptcinfo(im)
@ -22,7 +21,6 @@ def test_getiptcinfo_jpg_none():
def test_getiptcinfo_jpg_found(): def test_getiptcinfo_jpg_found():
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act # Act
iptc = IptcImagePlugin.getiptcinfo(im) iptc = IptcImagePlugin.getiptcinfo(im)
@ -35,7 +33,6 @@ def test_getiptcinfo_jpg_found():
def test_getiptcinfo_tiff_none(): def test_getiptcinfo_tiff_none():
# Arrange # Arrange
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
# Act # Act
iptc = IptcImagePlugin.getiptcinfo(im) iptc = IptcImagePlugin.getiptcinfo(im)

View File

@ -57,7 +57,6 @@ class TestFileJpeg:
return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode)))
def test_sanity(self): def test_sanity(self):
# internal version number # internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
@ -271,7 +270,10 @@ class TestFileJpeg:
# https://github.com/python-pillow/Pillow/issues/148 # https://github.com/python-pillow/Pillow/issues/148
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im = hopper() im = hopper()
im.save(f, "JPEG", quality=90, exif=b"1" * 65532) im.save(f, "JPEG", quality=90, exif=b"1" * 65533)
with pytest.raises(ValueError):
im.save(f, "JPEG", quality=90, exif=b"1" * 65534)
def test_exif_typeerror(self): def test_exif_typeerror(self):
with Image.open("Tests/images/exif_typeerror.jpg") as im: with Image.open("Tests/images/exif_typeerror.jpg") as im:
@ -368,7 +370,6 @@ class TestFileJpeg:
def test_exif_gps_typeerror(self): def test_exif_gps_typeerror(self):
with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
# Should not raise a TypeError # Should not raise a TypeError
im._getexif() im._getexif()
@ -447,7 +448,7 @@ class TestFileJpeg:
ims = im.get_child_images() ims = im.get_child_images()
assert len(ims) == 1 assert len(ims) == 1
assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1)
def test_mp(self): def test_mp(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
@ -682,7 +683,6 @@ class TestFileJpeg:
# Shouldn't raise error # Shouldn't raise error
fn = "Tests/images/sugarshack_bad_mpo_header.jpg" fn = "Tests/images/sugarshack_bad_mpo_header.jpg"
with pytest.warns(UserWarning, Image.open, fn) as im: with pytest.warns(UserWarning, Image.open, fn) as im:
# Assert # Assert
assert im.format == "JPEG" assert im.format == "JPEG"
@ -704,7 +704,6 @@ class TestFileJpeg:
# Arrange # Arrange
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
# Act # Act
im.save(outfile, "JPEG", dpi=im.info["dpi"]) im.save(outfile, "JPEG", dpi=im.info["dpi"])
@ -731,7 +730,6 @@ class TestFileJpeg:
# This Photoshop CC 2017 image has DPI in EXIF not metadata # This Photoshop CC 2017 image has DPI in EXIF not metadata
# EXIF XResolution is (2000000, 10000) # EXIF XResolution is (2000000, 10000)
with Image.open("Tests/images/photoshop-200dpi.jpg") as im: with Image.open("Tests/images/photoshop-200dpi.jpg") as im:
# Act / Assert # Act / Assert
assert im.info.get("dpi") == (200, 200) assert im.info.get("dpi") == (200, 200)
@ -740,7 +738,6 @@ class TestFileJpeg:
# This image has DPI in EXIF not metadata # This image has DPI in EXIF not metadata
# EXIF XResolution is 72 # EXIF XResolution is 72
with Image.open("Tests/images/exif-72dpi-int.jpg") as im: with Image.open("Tests/images/exif-72dpi-int.jpg") as im:
# Act / Assert # Act / Assert
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
@ -749,7 +746,6 @@ class TestFileJpeg:
# This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm:
# exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg
with Image.open("Tests/images/exif-200dpcm.jpg") as im: with Image.open("Tests/images/exif-200dpcm.jpg") as im:
# Act / Assert # Act / Assert
assert im.info.get("dpi") == (508, 508) assert im.info.get("dpi") == (508, 508)
@ -758,7 +754,6 @@ class TestFileJpeg:
# This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0:
# exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg
with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im:
# Act / Assert # Act / Assert
# This should return the default, and not raise a ZeroDivisionError # This should return the default, and not raise a ZeroDivisionError
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
@ -767,7 +762,6 @@ class TestFileJpeg:
# Arrange # Arrange
# 0x011A tag in this exif contains string '300300\x02' # 0x011A tag in this exif contains string '300300\x02'
with Image.open("Tests/images/broken_exif_dpi.jpg") as im: with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
# Act / Assert # Act / Assert
# This should return the default # This should return the default
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
@ -777,7 +771,6 @@ class TestFileJpeg:
# This is photoshop-200dpi.jpg with resolution removed from EXIF: # This is photoshop-200dpi.jpg with resolution removed from EXIF:
# exiftool "-*resolution*"= photoshop-200dpi.jpg # exiftool "-*resolution*"= photoshop-200dpi.jpg
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
# Act / Assert # Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated." # "When the image resolution is unknown, 72 [dpi] is designated."
# https://exiv2.org/tags.html # https://exiv2.org/tags.html
@ -787,7 +780,6 @@ class TestFileJpeg:
# This is no-dpi-in-exif with the tiff header of the exif block # This is no-dpi-in-exif with the tiff header of the exif block
# hexedited from MM * to FF FF FF FF # hexedited from MM * to FF FF FF FF
with Image.open("Tests/images/invalid-exif.jpg") as im: with Image.open("Tests/images/invalid-exif.jpg") as im:
# This should return the default, and not a SyntaxError or # This should return the default, and not a SyntaxError or
# OSError for unidentified image. # OSError for unidentified image.
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
@ -810,7 +802,6 @@ class TestFileJpeg:
def test_invalid_exif_x_resolution(self): def test_invalid_exif_x_resolution(self):
# When no x or y resolution is defined in EXIF # When no x or y resolution is defined in EXIF
with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im:
# This should return the default, and not a ValueError or # This should return the default, and not a ValueError or
# OSError for an unidentified image. # OSError for an unidentified image.
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
@ -820,7 +811,6 @@ class TestFileJpeg:
# This image has been manually hexedited to have an IFD offset of 10, # This image has been manually hexedited to have an IFD offset of 10,
# in contrast to normal 8 # in contrast to normal 8
with Image.open("Tests/images/exif-ifd-offset.jpg") as im: with Image.open("Tests/images/exif-ifd-offset.jpg") as im:
# Act / Assert # Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09" assert im._getexif()[306] == "2017:03:13 23:03:09"

View File

@ -270,7 +270,6 @@ def test_rgba():
# Arrange # Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2:
# Act # Act
j2k.load() j2k.load()
jp2.load() jp2.load()

View File

@ -645,7 +645,6 @@ class TestFileLibTiff(LibTiffTestCase):
pilim = hopper() pilim = hopper()
def save_bytesio(compression=None): def save_bytesio(compression=None):
buffer_io = io.BytesIO() buffer_io = io.BytesIO()
pilim.save(buffer_io, format="tiff", compression=compression) pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0) buffer_io.seek(0)
@ -740,7 +739,6 @@ class TestFileLibTiff(LibTiffTestCase):
def test_multipage_compression(self): def test_multipage_compression(self):
with Image.open("Tests/images/compression.tif") as im: with Image.open("Tests/images/compression.tif") as im:
im.seek(0) im.seek(0)
assert im._compression == "tiff_ccitt" assert im._compression == "tiff_ccitt"
assert im.size == (10, 10) assert im.size == (10, 10)

View File

@ -168,8 +168,7 @@ def test_mp_no_data():
def test_mp_attribute(test_file): def test_mp_attribute(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
frame_number = 0 for frame_number, mpentry in enumerate(mpinfo[0xB002]):
for mpentry in mpinfo[0xB002]:
mpattr = mpentry["Attribute"] mpattr = mpentry["Attribute"]
if frame_number: if frame_number:
assert not mpattr["RepresentativeImageFlag"] assert not mpattr["RepresentativeImageFlag"]
@ -180,7 +179,6 @@ def test_mp_attribute(test_file):
assert mpattr["ImageDataFormat"] == "JPEG" assert mpattr["ImageDataFormat"] == "JPEG"
assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
assert mpattr["Reserved"] == 0 assert mpattr["Reserved"] == 0
frame_number += 1
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)

View File

@ -44,7 +44,6 @@ def test_open_windows_v1():
# Arrange # Arrange
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
assert_image_equal(im, hopper("1")) assert_image_equal(im, hopper("1"))
assert isinstance(im, MspImagePlugin.MspImageFile) assert isinstance(im, MspImagePlugin.MspImageFile)
@ -59,7 +58,6 @@ def _assert_file_image_equal(source_path, target_path):
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
) )
def test_open_windows_v2(): def test_open_windows_v2():
files = ( files = (
os.path.join(EXTRA_DIR, f) os.path.join(EXTRA_DIR, f)
for f in os.listdir(EXTRA_DIR) for f in os.listdir(EXTRA_DIR)

View File

@ -80,6 +80,34 @@ def test_resolution(tmp_path):
assert size == (61.44, 61.44) assert size == (61.44, 61.44)
@pytest.mark.parametrize(
"params",
(
{"dpi": (75, 150)},
{"dpi": (75, 150), "resolution": 200},
),
)
def test_dpi(params, tmp_path):
im = hopper()
outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params)
with open(outfile, "rb") as fp:
contents = fp.read()
size = tuple(
float(d)
for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ")
)
assert size == (122.88, 61.44)
size = tuple(
float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
)
assert size == (122.88, 61.44)
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
@ -89,7 +117,6 @@ def test_save_all(tmp_path):
# Multiframe image # Multiframe image
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
im.save(outfile, save_all=True) im.save(outfile, save_all=True)
@ -123,7 +150,6 @@ def test_save_all(tmp_path):
def test_multiframe_normal_save(tmp_path): def test_multiframe_normal_save(tmp_path):
# Test saving a multiframe image without save_all # Test saving a multiframe image without save_all
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
outfile = str(tmp_path / "temp.pdf") outfile = str(tmp_path / "temp.pdf")
im.save(outfile) im.save(outfile)
@ -286,6 +312,7 @@ def test_pdf_append_to_bytesio():
@pytest.mark.timeout(1) @pytest.mark.timeout(1)
@pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower")
@pytest.mark.parametrize("newline", (b"\r", b"\n")) @pytest.mark.parametrize("newline", (b"\r", b"\n"))
def test_redos(newline): def test_redos(newline):
malicious = b" trailer<<>>" + newline * 3456 malicious = b" trailer<<>>" + newline * 3456

View File

@ -78,7 +78,6 @@ class TestFilePng:
return chunks return chunks
def test_sanity(self, tmp_path): def test_sanity(self, tmp_path):
# internal version number # internal version number
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
@ -156,7 +155,6 @@ class TestFilePng:
assert im.info == {"spam": "egg"} assert im.info == {"spam": "egg"}
def test_bad_itxt(self): def test_bad_itxt(self):
im = load(HEAD + chunk(b"iTXt") + TAIL) im = load(HEAD + chunk(b"iTXt") + TAIL)
assert im.info == {} assert im.info == {}
@ -201,7 +199,6 @@ class TestFilePng:
assert im.info["spam"].tkey == "Spam" assert im.info["spam"].tkey == "Spam"
def test_interlace(self): def test_interlace(self):
test_file = "Tests/images/pil123p.png" test_file = "Tests/images/pil123p.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert_image(im, "P", (162, 150)) assert_image(im, "P", (162, 150))
@ -495,7 +492,6 @@ class TestFilePng:
# Check reading images with null tRNS value, issue #1239 # Check reading images with null tRNS value, issue #1239
test_file = "Tests/images/tRNS_null_1x1.png" test_file = "Tests/images/tRNS_null_1x1.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.info["transparency"] == 0 assert im.info["transparency"] == 0
def test_save_icc_profile(self): def test_save_icc_profile(self):
@ -593,7 +589,7 @@ class TestFilePng:
def test_textual_chunks_after_idat(self): def test_textual_chunks_after_idat(self):
with Image.open("Tests/images/hopper.png") as im: with Image.open("Tests/images/hopper.png") as im:
assert "comment" in im.text.keys() assert "comment" in im.text
for k, v in { for k, v in {
"date:create": "2014-09-04T09:37:08+03:00", "date:create": "2014-09-04T09:37:08+03:00",
"date:modify": "2014-09-04T09:37:08+03:00", "date:modify": "2014-09-04T09:37:08+03:00",

View File

@ -77,7 +77,6 @@ def test_eoferror():
def test_seek_tell(): def test_seek_tell():
with Image.open(test_file) as im: with Image.open(test_file) as im:
layer_number = im.tell() layer_number = im.tell()
assert layer_number == 1 assert layer_number == 1
@ -95,7 +94,6 @@ def test_seek_tell():
def test_seek_eoferror(): def test_seek_eoferror():
with Image.open(test_file) as im: with Image.open(test_file) as im:
with pytest.raises(EOFError): with pytest.raises(EOFError):
im.seek(-1) im.seek(-1)

View File

@ -79,7 +79,6 @@ def test_is_spider_image():
def test_tell(): def test_tell():
# Arrange # Arrange
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Act # Act
index = im.tell() index = im.tell()

View File

@ -16,7 +16,6 @@ def test_sanity():
# Act # Act
with Image.open(test_file) as im: with Image.open(test_file) as im:
# Assert # Assert
assert im.size == (128, 128) assert im.size == (128, 128)

View File

@ -10,18 +10,21 @@ from .helper import is_pypy
TEST_TAR_FILE = "Tests/images/hopper.tar" TEST_TAR_FILE = "Tests/images/hopper.tar"
def test_sanity(): @pytest.mark.parametrize(
for codec, test_path, format in [ "codec, test_path, format",
["zlib", "hopper.png", "PNG"], (
["jpg", "hopper.jpg", "JPEG"], ("zlib", "hopper.png", "PNG"),
]: ("jpg", "hopper.jpg", "JPEG"),
if features.check(codec): ),
with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: )
with Image.open(tar) as im: def test_sanity(codec, test_path, format):
im.load() if features.check(codec):
assert im.mode == "RGB" with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar:
assert im.size == (128, 128) with Image.open(tar) as im:
assert im.format == format im.load()
assert im.mode == "RGB"
assert im.size == (128, 128)
assert im.format == format
@pytest.mark.skipif(is_pypy(), reason="Requires CPython") @pytest.mark.skipif(is_pypy(), reason="Requires CPython")

View File

@ -78,7 +78,6 @@ def test_id_field():
# Act # Act
with Image.open(test_file) as im: with Image.open(test_file) as im:
# Assert # Assert
assert im.size == (100, 100) assert im.size == (100, 100)
@ -89,7 +88,6 @@ def test_id_field_rle():
# Act # Act
with Image.open(test_file) as im: with Image.open(test_file) as im:
# Assert # Assert
assert im.size == (199, 199) assert im.size == (199, 199)
@ -171,7 +169,6 @@ def test_save_id_section(tmp_path):
test_file = "Tests/images/tga_id_field.tga" test_file = "Tests/images/tga_id_field.tga"
with Image.open(test_file) as im: with Image.open(test_file) as im:
# Save with no id section # Save with no id section
im.save(out, id_section="") im.save(out, id_section="")
with Image.open(out) as test_im: with Image.open(out) as test_im:

View File

@ -25,7 +25,6 @@ except ImportError:
class TestFileTiff: class TestFileTiff:
def test_sanity(self, tmp_path): def test_sanity(self, tmp_path):
filename = str(tmp_path / "temp.tif") filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename) hopper("RGB").save(filename)
@ -157,7 +156,6 @@ class TestFileTiff:
def test_xyres_tiff(self): def test_xyres_tiff(self):
filename = "Tests/images/pil168.tif" filename = "Tests/images/pil168.tif"
with Image.open(filename) as im: with Image.open(filename) as im:
# legacy api # legacy api
assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[X_RESOLUTION][0], tuple)
assert isinstance(im.tag[Y_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
@ -171,7 +169,6 @@ class TestFileTiff:
def test_xyres_fallback_tiff(self): def test_xyres_fallback_tiff(self):
filename = "Tests/images/compression.tif" filename = "Tests/images/compression.tif"
with Image.open(filename) as im: with Image.open(filename) as im:
# v2 api # v2 api
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
@ -186,7 +183,6 @@ class TestFileTiff:
def test_int_resolution(self): def test_int_resolution(self):
filename = "Tests/images/pil168.tif" filename = "Tests/images/pil168.tif"
with Image.open(filename) as im: with Image.open(filename) as im:
# Try to read a file where X,Y_RESOLUTION are ints # Try to read a file where X,Y_RESOLUTION are ints
im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[X_RESOLUTION] = 71
im.tag_v2[Y_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71
@ -381,7 +377,6 @@ class TestFileTiff:
def test___str__(self): def test___str__(self):
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im: with Image.open(filename) as im:
# Act # Act
ret = str(im.ifd) ret = str(im.ifd)
@ -392,7 +387,6 @@ class TestFileTiff:
# Arrange # Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im: with Image.open(filename) as im:
# v2 interface # v2 interface
v2_tags = { v2_tags = {
256: 55, 256: 55,
@ -630,7 +624,6 @@ class TestFileTiff:
filename = str(tmp_path / "temp.tif") filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs) hopper("RGB").save(filename, **kwargs)
with Image.open(filename) as im: with Image.open(filename) as im:
# legacy interface # legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[X_RESOLUTION][0][0] == 72
assert im.tag[Y_RESOLUTION][0][0] == 36 assert im.tag[Y_RESOLUTION][0][0] == 36

View File

@ -54,7 +54,6 @@ def test_rt_metadata(tmp_path):
img.save(f, tiffinfo=info) img.save(f, tiffinfo=info)
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
@ -74,14 +73,12 @@ def test_rt_metadata(tmp_path):
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
img.save(f, tiffinfo=info) img.save(f, tiffinfo=info)
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata(): def test_read_metadata():
with Image.open("Tests/images/hopper_g4.tif") as img: with Image.open("Tests/images/hopper_g4.tif") as img:
assert { assert {
"YResolution": IFDRational(4294967295, 113653537), "YResolution": IFDRational(4294967295, 113653537),
"PlanarConfiguration": 1, "PlanarConfiguration": 1,
@ -202,14 +199,15 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path):
assert reloaded.tag_v2[271] == expected assert reloaded.tag_v2[271] == expected
def test_writing_int_to_bytes(tmp_path): @pytest.mark.parametrize("value", (1, IFDRational(1)))
def test_writing_other_types_to_bytes(value, tmp_path):
im = hopper() im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[700] tag = TiffTags.TAGS_V2[700]
assert tag.type == TiffTags.BYTE assert tag.type == TiffTags.BYTE
info[700] = 1 info[700] = value
out = str(tmp_path / "temp.tiff") out = str(tmp_path / "temp.tiff")
im.save(out, tiffinfo=info) im.save(out, tiffinfo=info)
@ -218,6 +216,22 @@ def test_writing_int_to_bytes(tmp_path):
assert reloaded.tag_v2[700] == b"\x01" assert reloaded.tag_v2[700] == b"\x01"
def test_writing_other_types_to_undefined(tmp_path):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[33723]
assert tag.type == TiffTags.UNDEFINED
info[33723] = 1
out = str(tmp_path / "temp.tiff")
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert reloaded.tag_v2[33723] == b"1"
def test_undefined_zero(tmp_path): def test_undefined_zero(tmp_path):
# Check that the tag has not been changed since this test was created # Check that the tag has not been changed since this test was created
tag = TiffTags.TAGS_V2[45059] tag = TiffTags.TAGS_V2[45059]

View File

@ -18,10 +18,8 @@ except ImportError:
def test_read_exif_metadata(): def test_read_exif_metadata():
file_path = "Tests/images/flower.webp" file_path = "Tests/images/flower.webp"
with Image.open(file_path) as image: with Image.open(file_path) as image:
assert image.format == "WEBP" assert image.format == "WEBP"
exif_data = image.info.get("exif", None) exif_data = image.info.get("exif", None)
assert exif_data assert exif_data
@ -64,10 +62,8 @@ def test_write_exif_metadata():
def test_read_icc_profile(): def test_read_icc_profile():
file_path = "Tests/images/flower2.webp" file_path = "Tests/images/flower2.webp"
with Image.open(file_path) as image: with Image.open(file_path) as image:
assert image.format == "WEBP" assert image.format == "WEBP"
assert image.info.get("icc_profile", None) assert image.info.get("icc_profile", None)

View File

@ -6,7 +6,6 @@ from .helper import assert_image_similar_tofile, hopper
def test_load_raw(): def test_load_raw():
# Test basic EMF open and rendering # Test basic EMF open and rendering
with Image.open("Tests/images/drawing.emf") as im: with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"): if hasattr(Image.core, "drawwmf"):

View File

@ -44,7 +44,6 @@ def test_open():
# Act # Act
with Image.open(filename) as im: with Image.open(filename) as im:
# Assert # Assert
assert im.mode == "1" assert im.mode == "1"
assert im.size == (128, 128) assert im.size == (128, 128)
@ -57,7 +56,6 @@ def test_open_filename_with_underscore():
# Act # Act
with Image.open(filename) as im: with Image.open(filename) as im:
# Assert # Assert
assert im.mode == "1" assert im.mode == "1"
assert im.size == (128, 128) assert im.size == (128, 128)

View File

@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/hopper.p7"
def test_open(): def test_open():
# Act # Act
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
# Assert # Assert
assert im.format == "XVThumb" assert im.format == "XVThumb"

22
Tests/test_font_crash.py Normal file
View File

@ -0,0 +1,22 @@
import pytest
from PIL import Image, ImageDraw, ImageFont
from .helper import skip_unless_feature
class TestFontCrash:
def _fuzz_font(self, font):
# from fuzzers.fuzz_font
font.getbbox("ABC")
font.getmask("test text")
with Image.new(mode="RGBA", size=(200, 200)) as im:
draw = ImageDraw.Draw(im)
draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2)
draw.text((10, 10), "Test Text", font=font, fill="#000")
@skip_unless_feature("freetype2")
def test_segfault(self):
with pytest.raises(OSError):
font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784")
self._fuzz_font(font)

View File

@ -69,7 +69,6 @@ class TestImage:
assert issubclass(UnidentifiedImageError, OSError) assert issubclass(UnidentifiedImageError, OSError)
def test_sanity(self): def test_sanity(self):
im = Image.new("L", (100, 100)) im = Image.new("L", (100, 100))
assert repr(im)[:45] == "<PIL.Image.Image image mode=L size=100x100 at" assert repr(im)[:45] == "<PIL.Image.Image image mode=L size=100x100 at"
assert im.mode == "L" assert im.mode == "L"
@ -398,6 +397,17 @@ class TestImage:
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), (0, -1)) source.alpha_composite(over, (0, 0), (0, -1))
def test_register_open_duplicates(self):
# Arrange
factory, accept = Image.OPEN["JPEG"]
id_length = len(Image.ID)
# Act
Image.register_open("JPEG", factory, accept)
# Assert
assert len(Image.ID) == id_length
def test_registered_extensions_uninitialized(self): def test_registered_extensions_uninitialized(self):
# Arrange # Arrange
Image._initialized = 0 Image._initialized = 0
@ -512,6 +522,14 @@ class TestImage:
i = Image.new("RGB", [1, 1]) i = Image.new("RGB", [1, 1])
assert isinstance(i.size, tuple) assert isinstance(i.size, tuple)
@pytest.mark.timeout(0.75)
@pytest.mark.skipif(
"PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower"
)
@pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0)))
def test_empty_image(self, size):
Image.new("RGB", size)
def test_storage_neg(self): def test_storage_neg(self):
# Storage.c accepted negative values for xsize, ysize. Was # Storage.c accepted negative values for xsize, ysize. Was
# test_neg_ppm, but the core function for that has been # test_neg_ppm, but the core function for that has been
@ -921,12 +939,7 @@ class TestImage:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert Image.CONTAINER == 2 assert Image.CONTAINER == 2
def test_constants_deprecation(self): def test_constants(self):
with pytest.warns(DeprecationWarning):
assert Image.NEAREST == 0
with pytest.warns(DeprecationWarning):
assert Image.NONE == 0
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert Image.LINEAR == Image.Resampling.BILINEAR assert Image.LINEAR == Image.Resampling.BILINEAR
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
@ -943,8 +956,7 @@ class TestImage:
Image.Quantize, Image.Quantize,
): ):
for name in enum.__members__: for name in enum.__members__:
with pytest.warns(DeprecationWarning): assert getattr(Image, name) == enum[name]
assert getattr(Image, name) == enum[name]
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
@ -994,7 +1006,6 @@ def mock_encode(*args):
class TestRegistry: class TestRegistry:
def test_encode_registry(self): def test_encode_registry(self):
Image.register_encoder("MOCK", mock_encode) Image.register_encoder("MOCK", mock_encode)
assert "MOCK" in Image.ENCODERS assert "MOCK" in Image.ENCODERS

View File

@ -45,7 +45,6 @@ def test_unsupported_conversion():
def test_default(): def test_default():
im = hopper("P") im = hopper("P")
assert im.mode == "P" assert im.mode == "P"
converted_im = im.convert() converted_im = im.convert()

View File

@ -86,7 +86,6 @@ def test_crop_crash():
def test_crop_zero(): def test_crop_zero():
im = Image.new("RGB", (0, 0), "white") im = Image.new("RGB", (0, 0), "white")
cropped = im.crop((0, 0, 0, 0)) cropped = im.crop((0, 0, 0, 0))

View File

@ -24,6 +24,7 @@ from .helper import assert_image_equal, hopper
ImageFilter.ModeFilter, ImageFilter.ModeFilter,
ImageFilter.GaussianBlur, ImageFilter.GaussianBlur,
ImageFilter.GaussianBlur(5), ImageFilter.GaussianBlur(5),
ImageFilter.BoxBlur(0),
ImageFilter.BoxBlur(5), ImageFilter.BoxBlur(5),
ImageFilter.UnsharpMask, ImageFilter.UnsharpMask,
ImageFilter.UnsharpMask(10), ImageFilter.UnsharpMask(10),
@ -173,3 +174,14 @@ def test_consistency_5x5(mode):
Image.merge(mode, source[: len(mode)]).filter(kernel), Image.merge(mode, source[: len(mode)]).filter(kernel),
Image.merge(mode, reference[: len(mode)]), Image.merge(mode, reference[: len(mode)]),
) )
def test_invalid_box_blur_filter():
with pytest.raises(ValueError):
ImageFilter.BoxBlur(-2)
im = hopper()
box_blur_filter = ImageFilter.BoxBlur(2)
box_blur_filter.radius = -2
with pytest.raises(ValueError):
im.filter(box_blur_filter)

View File

@ -1,10 +1,17 @@
import pytest
from PIL import Image from PIL import Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
def test_sanity(): @pytest.mark.parametrize("data_type", ("bytes", "memoryview"))
def test_sanity(data_type):
im1 = hopper() im1 = hopper()
im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes())
data = im1.tobytes()
if data_type == "memoryview":
data = memoryview(data)
im2 = Image.frombytes(im1.mode, im1.size, data)
assert_image_equal(im1, im2) assert_image_equal(im1, im2)

View File

@ -4,7 +4,6 @@ from .helper import hopper
def test_sanity(): def test_sanity():
bbox = hopper().getbbox() bbox = hopper().getbbox()
assert isinstance(bbox, tuple) assert isinstance(bbox, tuple)

View File

@ -1,10 +1,11 @@
import pytest
from PIL import Image, ImageMode from PIL import Image, ImageMode
from .helper import hopper from .helper import hopper
def test_sanity(): def test_sanity():
with hopper() as im: with hopper() as im:
im.mode im.mode
@ -49,23 +50,25 @@ def test_sanity():
assert m.typestr == "|u1" assert m.typestr == "|u1"
def test_properties(): @pytest.mark.parametrize(
def check(mode, *result): "mode, expected_base, expected_type, expected_bands, expected_band_names",
signature = ( (
Image.getmodebase(mode), ("1", "L", "L", 1, ("1",)),
Image.getmodetype(mode), ("L", "L", "L", 1, ("L",)),
Image.getmodebands(mode), ("P", "P", "L", 1, ("P",)),
Image.getmodebandnames(mode), ("I", "L", "I", 1, ("I",)),
) ("F", "L", "F", 1, ("F",)),
assert signature == result ("RGB", "RGB", "L", 3, ("R", "G", "B")),
("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")),
check("1", "L", "L", 1, ("1",)) ("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")),
check("L", "L", "L", 1, ("L",)) ("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")),
check("P", "P", "L", 1, ("P",)) ("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")),
check("I", "L", "I", 1, ("I",)) ),
check("F", "L", "F", 1, ("F",)) )
check("RGB", "RGB", "L", 3, ("R", "G", "B")) def test_properties(
check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) mode, expected_base, expected_type, expected_bands, expected_band_names
check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) ):
check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) assert Image.getmodebase(mode) == expected_base
check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) assert Image.getmodetype(mode) == expected_type
assert Image.getmodebands(mode) == expected_bands
assert Image.getmodebandnames(mode) == expected_band_names

View File

@ -135,16 +135,15 @@ class TestImagingCoreResampleAccuracy:
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_bicubic(self, mode): def test_reduce_bicubic(self, mode):
for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (12, 12), 0xE1)
case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC)
case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off
# fmt: off data = ("e1 e3 d4"
data = ("e1 e3 d4" "e3 e5 d6"
"e3 e5 d6" "d4 d6 c9")
"d4 d6 c9") # fmt: on
# fmt: on for channel in case.split():
for channel in case.split(): self.check_case(channel, self.make_sample(data, (6, 6)))
self.check_case(channel, self.make_sample(data, (6, 6)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_lanczos(self, mode): def test_reduce_lanczos(self, mode):

View File

@ -52,7 +52,7 @@ def test_resample():
# >>> im.save('Tests/images/hopper_45.png') # >>> im.save('Tests/images/hopper_45.png')
with Image.open("Tests/images/hopper_45.png") as target: with Image.open("Tests/images/hopper_45.png") as target:
for (resample, epsilon) in ( for resample, epsilon in (
(Image.Resampling.NEAREST, 10), (Image.Resampling.NEAREST, 10),
(Image.Resampling.BILINEAR, 5), (Image.Resampling.BILINEAR, 5),
(Image.Resampling.BICUBIC, 0), (Image.Resampling.BICUBIC, 0),

View File

@ -4,7 +4,6 @@ from .helper import assert_image_equal, fromstring, hopper
def test_sanity(): def test_sanity():
with pytest.raises(ValueError): with pytest.raises(ValueError):
hopper().tobitmap() hopper().tobitmap()

View File

@ -42,12 +42,12 @@ class TestImageTransform:
def test_extent(self): def test_extent(self):
im = hopper("RGB") im = hopper("RGB")
(w, h) = im.size (w, h) = im.size
# fmt: off transformed = im.transform(
transformed = im.transform(im.size, Image.Transform.EXTENT, im.size,
(0, 0, Image.Transform.EXTENT,
w//2, h//2), # ul -> lr (0, 0, w // 2, h // 2), # ul -> lr
Image.Resampling.BILINEAR) Image.Resampling.BILINEAR,
# fmt: on )
scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h))
@ -58,13 +58,12 @@ class TestImageTransform:
# one simple quad transform, equivalent to scale & crop upper left quad # one simple quad transform, equivalent to scale & crop upper left quad
im = hopper("RGB") im = hopper("RGB")
(w, h) = im.size (w, h) = im.size
# fmt: off transformed = im.transform(
transformed = im.transform(im.size, Image.Transform.QUAD, im.size,
(0, 0, 0, h//2, Image.Transform.QUAD,
# ul -> ccw around quad: (0, 0, 0, h // 2, w // 2, h // 2, w // 2, 0), # ul -> ccw around quad
w//2, h//2, w//2, 0), Image.Resampling.BILINEAR,
Image.Resampling.BILINEAR) )
# fmt: on
scaled = im.transform( scaled = im.transform(
(w, h), (w, h),
@ -99,16 +98,21 @@ class TestImageTransform:
# this should be a checkerboard of halfsized hoppers in ul, lr # this should be a checkerboard of halfsized hoppers in ul, lr
im = hopper("RGBA") im = hopper("RGBA")
(w, h) = im.size (w, h) = im.size
# fmt: off transformed = im.transform(
transformed = im.transform(im.size, Image.Transform.MESH, im.size,
[((0, 0, w//2, h//2), # box Image.Transform.MESH,
(0, 0, 0, h, (
w, h, w, 0)), # ul -> ccw around quad (
((w//2, h//2, w, h), # box (0, 0, w // 2, h // 2), # box
(0, 0, 0, h, (0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad
w, h, w, 0))], # ul -> ccw around quad ),
Image.Resampling.BILINEAR) (
# fmt: on (w // 2, h // 2, w, h), # box
(0, 0, 0, h, w, h, w, 0), # ul -> ccw around quad
),
),
Image.Resampling.BILINEAR,
)
scaled = im.transform( scaled = im.transform(
(w // 2, h // 2), (w // 2, h // 2),
@ -174,11 +178,13 @@ class TestImageTransform:
im = op(im, (40, 10)) im = op(im, (40, 10))
colors = im.getcolors() colors = sorted(im.getcolors())
assert colors == [ assert colors == sorted(
(20 * 10, opaque), (
(20 * 10, transparent), (20 * 10, opaque),
] (20 * 10, transparent),
)
)
@pytest.mark.parametrize("mode", ("RGBA", "LA")) @pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_resize(self, mode): def test_nearest_resize(self, mode):

View File

@ -50,7 +50,6 @@ def test_add():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
# Act # Act
new = ImageChops.add(im1, im2) new = ImageChops.add(im1, im2)
@ -63,7 +62,6 @@ def test_add_scale_offset():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
# Act # Act
new = ImageChops.add(im1, im2, scale=2.5, offset=100) new = ImageChops.add(im1, im2, scale=2.5, offset=100)
@ -87,7 +85,6 @@ def test_add_modulo():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
# Act # Act
new = ImageChops.add_modulo(im1, im2) new = ImageChops.add_modulo(im1, im2)
@ -111,7 +108,6 @@ def test_blend():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
# Act # Act
new = ImageChops.blend(im1, im2, 0.5) new = ImageChops.blend(im1, im2, 0.5)
@ -137,7 +133,6 @@ def test_darker_image():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.darker(im1, im2) new = ImageChops.darker(im1, im2)
@ -149,7 +144,6 @@ def test_darker_pixel():
# Arrange # Arrange
im1 = hopper() im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.darker(im1, im2) new = ImageChops.darker(im1, im2)
@ -161,7 +155,6 @@ def test_difference():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1:
with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2:
# Act # Act
new = ImageChops.difference(im1, im2) new = ImageChops.difference(im1, im2)
@ -173,7 +166,6 @@ def test_difference_pixel():
# Arrange # Arrange
im1 = hopper() im1 = hopper()
with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2:
# Act # Act
new = ImageChops.difference(im1, im2) new = ImageChops.difference(im1, im2)
@ -195,7 +187,6 @@ def test_duplicate():
def test_invert(): def test_invert():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im:
# Act # Act
new = ImageChops.invert(im) new = ImageChops.invert(im)
@ -209,7 +200,6 @@ def test_lighter_image():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.lighter(im1, im2) new = ImageChops.lighter(im1, im2)
@ -221,7 +211,6 @@ def test_lighter_pixel():
# Arrange # Arrange
im1 = hopper() im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.lighter(im1, im2) new = ImageChops.lighter(im1, im2)
@ -275,7 +264,6 @@ def test_offset():
xoffset = 45 xoffset = 45
yoffset = 20 yoffset = 20
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im:
# Act # Act
new = ImageChops.offset(im, xoffset, yoffset) new = ImageChops.offset(im, xoffset, yoffset)
@ -292,7 +280,6 @@ def test_screen():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2:
# Act # Act
new = ImageChops.screen(im1, im2) new = ImageChops.screen(im1, im2)
@ -305,7 +292,6 @@ def test_subtract():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.subtract(im1, im2) new = ImageChops.subtract(im1, im2)
@ -319,7 +305,6 @@ def test_subtract_scale_offset():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) new = ImageChops.subtract(im1, im2, scale=2.5, offset=100)
@ -332,7 +317,6 @@ def test_subtract_clip():
# Arrange # Arrange
im1 = hopper() im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.subtract(im1, im2) new = ImageChops.subtract(im1, im2)
@ -344,7 +328,6 @@ def test_subtract_modulo():
# Arrange # Arrange
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1:
with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.subtract_modulo(im1, im2) new = ImageChops.subtract_modulo(im1, im2)
@ -358,7 +341,6 @@ def test_subtract_modulo_no_clip():
# Arrange # Arrange
im1 = hopper() im1 = hopper()
with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2:
# Act # Act
new = ImageChops.subtract_modulo(im1, im2) new = ImageChops.subtract_modulo(im1, im2)
@ -370,7 +352,6 @@ def test_soft_light():
# Arrange # Arrange
with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2: with Image.open("Tests/images/hopper-XYZ.png") as im2:
# Act # Act
new = ImageChops.soft_light(im1, im2) new = ImageChops.soft_light(im1, im2)
@ -383,7 +364,6 @@ def test_hard_light():
# Arrange # Arrange
with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2: with Image.open("Tests/images/hopper-XYZ.png") as im2:
# Act # Act
new = ImageChops.hard_light(im1, im2) new = ImageChops.hard_light(im1, im2)
@ -396,7 +376,6 @@ def test_overlay():
# Arrange # Arrange
with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper.png") as im1:
with Image.open("Tests/images/hopper-XYZ.png") as im2: with Image.open("Tests/images/hopper-XYZ.png") as im2:
# Act # Act
new = ImageChops.overlay(im1, im2) new = ImageChops.overlay(im1, im2)

View File

@ -52,7 +52,6 @@ def test_sanity():
def test_valueerror(): def test_valueerror():
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
draw.line((0, 0), fill=(0, 0, 0)) draw.line((0, 0), fill=(0, 0, 0))

View File

@ -30,7 +30,6 @@ SAFEBLOCK = ImageFile.SAFEBLOCK
class TestImageFile: class TestImageFile:
def test_parser(self): def test_parser(self):
def roundtrip(format): def roundtrip(format):
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"): if format in ("MSP", "XBM"):
im = im.convert("1") im = im.convert("1")

View File

@ -64,7 +64,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
) )
p.communicate() p.communicate()
else: else:
if not shutil.which("wl-paste"): if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises( with pytest.raises(
NotImplementedError, NotImplementedError,
match="wl-paste or xclip is required for" match="wl-paste or xclip is required for"

View File

@ -21,7 +21,6 @@ deformer = Deformer()
def test_sanity(): def test_sanity():
ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("L"))
ImageOps.autocontrast(hopper("RGB")) ImageOps.autocontrast(hopper("RGB"))
@ -419,7 +418,6 @@ def test_autocontrast_cutoff():
def test_autocontrast_mask_toy_input(): def test_autocontrast_mask_toy_input():
# Test the mask argument of autocontrast # Test the mask argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img: with Image.open("Tests/images/bw_gradient.png") as img:
rect_mask = Image.new("L", img.size, 0) rect_mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(rect_mask) draw = ImageDraw.Draw(rect_mask)
x0 = img.size[0] // 4 x0 = img.size[0] // 4
@ -439,7 +437,6 @@ def test_autocontrast_mask_toy_input():
def test_autocontrast_mask_real_input(): def test_autocontrast_mask_real_input():
# Test the autocontrast with a rectangular mask # Test the autocontrast with a rectangular mask
with Image.open("Tests/images/iptc.jpg") as img: with Image.open("Tests/images/iptc.jpg") as img:
rect_mask = Image.new("L", img.size, 0) rect_mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(rect_mask) draw = ImageDraw.Draw(rect_mask)
x0, y0 = img.size[0] // 2, img.size[1] // 2 x0, y0 = img.size[0] // 2, img.size[1] // 2

View File

@ -6,7 +6,6 @@ from .helper import assert_image_equal, assert_image_equal_tofile
def test_sanity(): def test_sanity():
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
assert len(palette.colors) == 256 assert len(palette.colors) == 256
@ -23,7 +22,6 @@ def test_reload():
def test_getcolor(): def test_getcolor():
palette = ImagePalette.ImagePalette() palette = ImagePalette.ImagePalette()
assert len(palette.palette) == 0 assert len(palette.palette) == 0
assert len(palette.colors) == 0 assert len(palette.colors) == 0
@ -84,7 +82,6 @@ def test_getcolor_not_special(index, palette):
def test_file(tmp_path): def test_file(tmp_path):
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
f = str(tmp_path / "temp.lut") f = str(tmp_path / "temp.lut")

View File

@ -8,7 +8,6 @@ from PIL import Image, ImagePath
def test_path(): def test_path():
p = ImagePath.Path(list(range(10))) p = ImagePath.Path(list(range(10)))
# sequence interface # sequence interface
@ -58,10 +57,7 @@ def test_path():
assert list(p) == [(0.0, 1.0)] assert list(p) == [(0.0, 1.0)]
arr = array.array("f", [0, 1]) arr = array.array("f", [0, 1])
if hasattr(arr, "tobytes"): p = ImagePath.Path(arr.tobytes())
p = ImagePath.Path(arr.tobytes())
else:
p = ImagePath.Path(arr.tostring())
assert list(p) == [(0.0, 1.0)] assert list(p) == [(0.0, 1.0)]

View File

@ -6,7 +6,6 @@ from .helper import assert_image_equal, hopper, skip_unless_feature
def test_sanity(tmp_path): def test_sanity(tmp_path):
test_file = str(tmp_path / "temp.im") test_file = str(tmp_path / "temp.im")
im = hopper("RGB") im = hopper("RGB")

View File

@ -6,7 +6,6 @@ from .helper import hopper
def test_sanity(): def test_sanity():
im = hopper() im = hopper()
st = ImageStat.Stat(im) st = ImageStat.Stat(im)
@ -31,7 +30,6 @@ def test_sanity():
def test_hopper(): def test_hopper():
im = hopper() im = hopper()
st = ImageStat.Stat(im) st = ImageStat.Stat(im)
@ -45,7 +43,6 @@ def test_hopper():
def test_constant(): def test_constant():
im = Image.new("L", (128, 128), 128) im = Image.new("L", (128, 128), 128)
st = ImageStat.Stat(im) st = ImageStat.Stat(im)

View File

@ -4,7 +4,6 @@ from PIL import Image
def test_setmode(): def test_setmode():
im = Image.new("L", (1, 1), 255) im = Image.new("L", (1, 1), 255)
im.im.setmode("1") im.im.setmode("1")
assert im.im.getpixel((0, 0)) == 255 assert im.im.getpixel((0, 0)) == 255

View File

@ -42,7 +42,6 @@ def test_basic(tmp_path, mode):
im_in.save(filename) im_in.save(filename)
with Image.open(filename) as im_out: with Image.open(filename) as im_out:
verify(im_in) verify(im_in)
verify(im_out) verify(im_out)
@ -87,7 +86,6 @@ def test_tobytes():
def test_convert(): def test_convert():
im = original.copy() im = original.copy()
verify(im.convert("I;16")) verify(im.convert("I;16"))

View File

@ -235,7 +235,6 @@ def test_no_resource_warning_for_numpy_array():
test_file = "Tests/images/hopper.png" test_file = "Tests/images/hopper.png"
with Image.open(test_file) as im: with Image.open(test_file) as im:
# Act/Assert # Act/Assert
with warnings.catch_warnings(): with warnings.catch_warnings():
array(im) array(im)

View File

@ -88,9 +88,8 @@ def test_parsing():
b"D:20180729214124+08'00'": "20180729134124", b"D:20180729214124+08'00'": "20180729134124",
b"D:20180729214124-05'00'": "20180730024124", b"D:20180729214124-05'00'": "20180730024124",
}.items(): }.items():
d = PdfParser.get_value(b"<</" + name.encode() + b" (" + date + b")>>", 0)[ b = b"<</" + name.encode() + b" (" + date + b")>>"
0 d = PdfParser.get_value(b, 0)[0]
]
assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value

View File

@ -89,7 +89,6 @@ def test_pickle_la_mode_with_palette(tmp_path):
def test_pickle_tell(): def test_pickle_tell():
# Arrange # Arrange
with Image.open("Tests/images/hopper.webp") as image: with Image.open("Tests/images/hopper.webp") as image:
# Act: roundtrip # Act: roundtrip
unpickled_image = pickle.loads(pickle.dumps(image)) unpickled_image = pickle.loads(pickle.dumps(image))

View File

@ -6,7 +6,7 @@ with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning) warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper from .helper import assert_image_equal_tofile, assert_image_similar, hopper
if ImageQt.qt_is_installed: if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap from PIL.ImageQt import QPixmap
@ -48,7 +48,7 @@ if ImageQt.qt_is_installed:
def roundtrip(expected): def roundtrip(expected):
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb # Qt saves all pixmaps as rgb
assert_image_equal(result, expected.convert("RGB")) assert_image_similar(result, expected.convert("RGB"), 1)
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")

View File

@ -7,7 +7,6 @@ from .helper import hopper
def _test_equal(num, denom, target): def _test_equal(num, denom, target):
t = IFDRational(num, denom) t = IFDRational(num, denom)
assert target == t assert target == t
@ -15,7 +14,6 @@ def _test_equal(num, denom, target):
def test_sanity(): def test_sanity():
_test_equal(1, 1, 1) _test_equal(1, 1, 1)
_test_equal(1, 1, Fraction(1, 1)) _test_equal(1, 1, Fraction(1, 1))

View File

@ -9,7 +9,6 @@ test_file = "Tests/images/hopper.webp"
@skip_unless_feature("webp") @skip_unless_feature("webp")
class TestWebPLeaks(PillowLeakTestCase): class TestWebPLeaks(PillowLeakTestCase):
mem_limit = 3 * 1024 # kb mem_limit = 3 * 1024 # kb
iterations = 100 iterations = 100

View File

@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then
wget -O $archive.tar.gz $url wget -O $archive.tar.gz $url
fi fi
rm -r $archive rmdir $archive
tar -xvzf $archive.tar.gz tar -xvzf $archive.tar.gz

View File

@ -1,15 +1,12 @@
#!/bin/bash #!/usr/bin/env bash
# install extra test images # install extra test images
# Use SVN to just fetch a single Git subdirectory archive=test-images-main
svn_export()
{
if [ ! -z $1 ]; then
echo ""
echo "Retrying svn export..."
echo ""
fi
svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images ./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz
}
svn_export || svn_export retry || svn_export retry || svn_export retry mv $archive/* ../Tests/images/
# Cleanup old tarball and empty directory
rm $archive.tar.gz
rmdir $archive

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install libimagequant # install libimagequant
archive=libimagequant-4.0.4 archive=libimagequant-4.1.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -2,7 +2,7 @@
# install raqm # install raqm
archive=libraqm-0.9.0 archive=libraqm-0.10.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install webp # install webp
archive=libwebp-1.2.4 archive=libwebp-1.3.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is Pillow is the friendly PIL fork. It is
Copyright © 2010-2022 by Alex Clark and contributors Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors
Like PIL, Pillow is licensed under the open source PIL Like PIL, Pillow is licensed under the open source PIL
Software License: Software License:

View File

@ -52,8 +52,10 @@ master_doc = "index"
# General information about the project. # General information about the project.
project = "Pillow (PIL Fork)" project = "Pillow (PIL Fork)"
copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors" copyright = (
author = "Fredrik Lundh, Alex Clark and Contributors" "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors"
)
author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -243,7 +245,7 @@ latex_documents = [
master_doc, master_doc,
"PillowPILFork.tex", "PillowPILFork.tex",
"Pillow (PIL Fork) Documentation", "Pillow (PIL Fork) Documentation",
"Alex Clark", "Jeffrey A. Clark (Alex)",
"manual", "manual",
) )
] ]
@ -293,7 +295,7 @@ texinfo_documents = [
"Pillow (PIL Fork) Documentation", "Pillow (PIL Fork) Documentation",
author, author,
"PillowPILFork", "PillowPILFork",
"Pillow is the friendly PIL fork by Alex Clark and Contributors.", "Pillow is the friendly PIL fork by Jeffrey A. Clark (Alex) and contributors.",
"Miscellaneous", "Miscellaneous",
) )
] ]

View File

@ -74,40 +74,18 @@ Constants
A number of constants have been deprecated and will be removed in Pillow 10.0.0 A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. (2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
.. note::
Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that
was reversed in Pillow 9.4.0 and those constants will now remain available.
See :ref:`restored-image-constants`
===================================================== ============================================================ ===================================================== ============================================================
Deprecated Use instead Deprecated Use instead
===================================================== ============================================================ ===================================================== ============================================================
``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` ``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR``
``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` ``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC``
``Image.ORDERED`` ``Image.Dither.ORDERED`` ``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS``
``Image.RASTERIZE`` ``Image.Dither.RASTERIZE``
``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG``
``Image.WEB`` ``Image.Palette.WEB``
``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE``
``Image.AFFINE`` ``Image.Transform.AFFINE``
``Image.EXTENT`` ``Image.Transform.EXTENT``
``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE``
``Image.QUAD`` ``Image.Transform.QUAD``
``Image.MESH`` ``Image.Transform.MESH``
``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT``
``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM``
``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90``
``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180``
``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270``
``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE``
``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE``
``Image.BOX`` ``Image.Resampling.BOX``
``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``
``Image.LANCZOS`` ``Image.Resampling.LANCZOS``
``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS``
``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT``
``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE``
``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE``
``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT``
``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` ``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL``
``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` ``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC``
``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` ``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION``

View File

@ -31,7 +31,7 @@ INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current re
supports the following standard modes: supports the following standard modes:
* ``1`` (1-bit pixels, black and white, stored with one pixel per byte) * ``1`` (1-bit pixels, black and white, stored with one pixel per byte)
* ``L`` (8-bit pixels, black and white) * ``L`` (8-bit pixels, grayscale)
* ``P`` (8-bit pixels, mapped to any other mode using a color palette) * ``P`` (8-bit pixels, mapped to any other mode using a color palette)
* ``RGB`` (3x8-bit pixels, true color) * ``RGB`` (3x8-bit pixels, true color)
* ``RGBA`` (4x8-bit pixels, true color with transparency mask) * ``RGBA`` (4x8-bit pixels, true color with transparency mask)
@ -148,44 +148,44 @@ pixel, the Python Imaging Library provides different resampling *filters*.
.. py:currentmodule:: PIL.Image .. py:currentmodule:: PIL.Image
.. data:: NEAREST .. data:: Resampling.NEAREST
Pick one nearest pixel from the input image. Ignore all other input pixels. Pick one nearest pixel from the input image. Ignore all other input pixels.
.. data:: BOX .. data:: Resampling.BOX
Each pixel of source image contributes to one pixel of the Each pixel of source image contributes to one pixel of the
destination image with identical weights. destination image with identical weights.
For upscaling is equivalent of :data:`NEAREST`. For upscaling is equivalent of :data:`Resampling.NEAREST`.
This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` This filter can only be used with the :py:meth:`~PIL.Image.Image.resize`
and :py:meth:`~PIL.Image.Image.thumbnail` methods. and :py:meth:`~PIL.Image.Image.thumbnail` methods.
.. versionadded:: 3.4.0 .. versionadded:: 3.4.0
.. data:: BILINEAR .. data:: Resampling.BILINEAR
For resize calculate the output pixel value using linear interpolation For resize calculate the output pixel value using linear interpolation
on all pixels that may contribute to the output value. on all pixels that may contribute to the output value.
For other transformations linear interpolation over a 2x2 environment For other transformations linear interpolation over a 2x2 environment
in the input image is used. in the input image is used.
.. data:: HAMMING .. data:: Resampling.HAMMING
Produces a sharper image than :data:`BILINEAR`, doesn't have dislocations Produces a sharper image than :data:`Resampling.BILINEAR`, doesn't have
on local level like with :data:`BOX`. dislocations on local level like with :data:`Resampling.BOX`.
This filter can only be used with the :py:meth:`~PIL.Image.Image.resize` This filter can only be used with the :py:meth:`~PIL.Image.Image.resize`
and :py:meth:`~PIL.Image.Image.thumbnail` methods. and :py:meth:`~PIL.Image.Image.thumbnail` methods.
.. versionadded:: 3.4.0 .. versionadded:: 3.4.0
.. data:: BICUBIC .. data:: Resampling.BICUBIC
For resize calculate the output pixel value using cubic interpolation For resize calculate the output pixel value using cubic interpolation
on all pixels that may contribute to the output value. on all pixels that may contribute to the output value.
For other transformations cubic interpolation over a 4x4 environment For other transformations cubic interpolation over a 4x4 environment
in the input image is used. in the input image is used.
.. data:: LANCZOS .. data:: Resampling.LANCZOS
Calculate the output pixel value using a high-quality Lanczos filter (a Calculate the output pixel value using a high-quality Lanczos filter (a
truncated sinc) on all pixels that may contribute to the output value. truncated sinc) on all pixels that may contribute to the output value.
@ -198,19 +198,19 @@ pixel, the Python Imaging Library provides different resampling *filters*.
Filters comparison table Filters comparison table
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
+----------------+-------------+-----------+-------------+ +---------------------------+-------------+-----------+-------------+
| Filter | Downscaling | Upscaling | Performance | | Filter | Downscaling | Upscaling | Performance |
| | quality | quality | | | | quality | quality | |
+================+=============+===========+=============+ +===========================+=============+===========+=============+
|:data:`NEAREST` | | | ⭐⭐⭐⭐⭐ | |:data:`Resampling.NEAREST` | | | ⭐⭐⭐⭐⭐ |
+----------------+-------------+-----------+-------------+ +---------------------------+-------------+-----------+-------------+
|:data:`BOX` | ⭐ | | ⭐⭐⭐⭐ | |:data:`Resampling.BOX` | ⭐ | | ⭐⭐⭐⭐ |
+----------------+-------------+-----------+-------------+ +---------------------------+-------------+-----------+-------------+
|:data:`BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ | |:data:`Resampling.BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ |
+----------------+-------------+-----------+-------------+ +---------------------------+-------------+-----------+-------------+
|:data:`HAMMING` | ⭐⭐ | | ⭐⭐⭐ | |:data:`Resampling.HAMMING` | ⭐⭐ | | ⭐⭐⭐ |
+----------------+-------------+-----------+-------------+ +---------------------------+-------------+-----------+-------------+
|:data:`BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | |:data:`Resampling.BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
+----------------+-------------+-----------+-------------+ +---------------------------+-------------+-----------+-------------+
|:data:`LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | |:data:`Resampling.LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
+----------------+-------------+-----------+-------------+ +---------------------------+-------------+-----------+-------------+

View File

@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo.
Either an integer or a float. Either an integer or a float.
**dpi** **dpi**
A tuple of (x_resolution, y_resolution), with inches as the resolution A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution
unit. For consistency with other image formats, the x and y resolutions unit. For consistency with other image formats, the x and y resolutions
of the dpi will be rounded to the nearest integer. of the dpi will be rounded to the nearest integer.
@ -1126,7 +1126,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
If present and true, instructs the WebP writer to use lossless compression. If present and true, instructs the WebP writer to use lossless compression.
**quality** **quality**
Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest
size and 100 the largest. For lossless, this parameter is the amount size and 100 the largest. For lossless, this parameter is the amount
of effort put into the compression: 0 is the fastest, but gives larger of effort put into the compression: 0 is the fastest, but gives larger
files compared to the slowest, but best, 100. files compared to the slowest, but best, 100.
@ -1147,6 +1147,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
The exif data to include in the saved file. Only supported if The exif data to include in the saved file. Only supported if
the system WebP library was built with webpmux support. the system WebP library was built with webpmux support.
**xmp**
The XMP data to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
Saving sequences Saving sequences
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
@ -1493,6 +1497,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
image, will determine the physical dimensions of the page that will be image, will determine the physical dimensions of the page that will be
saved in the PDF. saved in the PDF.
**dpi**
A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution
unit. If both the ``resolution`` parameter and the ``dpi`` parameter are
present, ``resolution`` will be ignored.
**title** **title**
The documents title. If not appending to an existing PDF file, this will The documents title. If not appending to an existing PDF file, this will
default to the filename. default to the filename.

View File

@ -1,7 +1,7 @@
Pillow Pillow
====== ======
Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.com/python-pillow/Pillow/graphs/contributors>`_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. Pillow is the friendly PIL fork by `Jeffrey A. Clark (Alex) and contributors <https://github.com/python-pillow/Pillow/graphs/contributors>`_. PIL is the Python Imaging Library by Fredrik Lundh and contributors.
Pillow for enterprise is available via the Tidelift Subscription. `Learn more <https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=docs&utm_campaign=enterprise>`_. Pillow for enterprise is available via the Tidelift Subscription. `Learn more <https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=docs&utm_campaign=enterprise>`_.
@ -73,6 +73,22 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://bestpractices.coreinfrastructure.org/projects/6331 :target: https://bestpractices.coreinfrastructure.org/projects/6331
:alt: OpenSSF Best Practices :alt: OpenSSF Best Practices
.. image:: https://badges.gitter.im/python-pillow/Pillow.svg
:target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
:alt: Join the chat at https://gitter.im/python-pillow/Pillow
.. image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg
:target: https://twitter.com/PythonPillow
:alt: Follow on https://twitter.com/PythonPillow
.. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg
:target: https://fosstodon.org/@pillow
:alt: Follow on https://fosstodon.org/@pillow
.. raw:: html
<link rel="me" href="https://fosstodon.org/@pillow">
Overview Overview
======== ========

View File

@ -169,7 +169,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.0.4** * Pillow has been tested with libimagequant **2.6-4.1**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.
@ -369,21 +369,21 @@ Build Options
available, as many as are present. available, as many as are present.
* Build flags: ``--disable-zlib``, ``--disable-jpeg``, * Build flags: ``--disable-zlib``, ``--disable-jpeg``,
``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``,
``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``,
``--disable-imagequant``, ``--disable-xcb``. ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``.
Disable building the corresponding feature even if the development Disable building the corresponding feature even if the development
libraries are present on the building machine. libraries are present on the building machine.
* Build flags: ``--enable-zlib``, ``--enable-jpeg``, * Build flags: ``--enable-zlib``, ``--enable-jpeg``,
``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``,
``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``,
``--enable-imagequant``, ``--enable-xcb``. ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``.
Require that the corresponding feature is built. The build will raise Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata) an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together. relies on WebP support. Tcl and Tk also must be used together.
* Build flags: ``--vendor-raqm --vendor-fribidi`` * Build flags: ``--vendor-raqm``, ``--vendor-fribidi``.
These flags are used to compile a modified version of libraqm and These flags are used to compile a modified version of libraqm and
a shim that dynamically loads libfribidi at runtime. These are a shim that dynamically loads libfribidi at runtime. These are
used to compile the standard Pillow wheels. Compiling libraqm requires used to compile the standard Pillow wheels. Compiling libraqm requires
@ -442,21 +442,23 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 | | Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | | macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | PyPy3 | | | | 3.12, PyPy3 | |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | | Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | | Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | | s390x, x86-64 | | | 3.12, PyPy3 | |
| +----------------------------+---------------------+
| | 3.10 | arm64v8, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.7 | x86-64 | | Windows Server 2016 | 3.7 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | | Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 |
| | PyPy3 | | | | 3.12, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9 (MinGW) | x86, x86-64 | | | 3.9 (MinGW) | x86, x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
@ -478,13 +480,13 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+ +==================================+===========================+==================+==============+
| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | | macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |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, 3.11 | 9.3.0 |x86-64 | | | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
| +---------------------------+------------------+ | | +---------------------------+------------------+ |
| | 3.6 | 8.4.0 | | | | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+

View File

@ -430,6 +430,7 @@ See :ref:`concept-filters` for details.
.. autoclass:: Resampling .. autoclass:: Resampling
:members: :members:
:undoc-members: :undoc-members:
:noindex:
Some deprecated filters are also available under the following names: Some deprecated filters are also available under the following names:

View File

@ -29,84 +29,78 @@ Image resizing filters
Image resizing methods :py:meth:`~PIL.Image.Image.resize` and Image resizing methods :py:meth:`~PIL.Image.Image.resize` and
:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells :py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells
which filter should be used for resampling. Possible values are: which filter should be used for resampling. Possible values are:
:py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BILINEAR`, ``NEAREST``, ``BILINEAR``, ``BICUBIC`` and ``ANTIALIAS``. Almost all of them
:py:data:`PIL.Image.BICUBIC` and :py:data:`PIL.Image.ANTIALIAS`. were changed in this version.
Almost all of them were changed in this version.
Bicubic and bilinear downscaling Bicubic and bilinear downscaling
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
From the beginning :py:data:`~PIL.Image.BILINEAR` and From the beginning ``BILINEAR`` and ``BICUBIC`` filters were based on affine
:py:data:`~PIL.Image.BICUBIC` filters were based on affine transformations transformations and used a fixed number of pixels from the source image for
and used a fixed number of pixels from the source image for every destination every destination pixel (2x2 pixels for ``BILINEAR`` and 4x4 for ``BICUBIC``).
pixel (2x2 pixels for :py:data:`~PIL.Image.BILINEAR` and 4x4 for This gave an unsatisfactory result for downscaling. At the same time, a high
:py:data:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for quality convolutions-based algorithm with flexible kernel was used for
downscaling. At the same time, a high quality convolutions-based algorithm with ``ANTIALIAS`` filter.
flexible kernel was used for :py:data:`~PIL.Image.ANTIALIAS` filter.
Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used
for all of these three filters. for all of these three filters.
If you have previously used any tricks to maintain quality when downscaling with If you have previously used any tricks to maintain quality when downscaling with
:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters ``BILINEAR`` and ``BICUBIC`` filters (for example, reducing within several
(for example, reducing within several steps), they are unnecessary now. steps), they are unnecessary now.
Antialias renamed to Lanczos Antialias renamed to Lanczos
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A new :py:data:`PIL.Image.LANCZOS` constant was added instead of A new ``LANCZOS`` constant was added instead of ``ANTIALIAS``.
:py:data:`~PIL.Image.ANTIALIAS`.
When :py:data:`~PIL.Image.ANTIALIAS` was initially added, it was the only When ``ANTIALIAS`` was initially added, it was the only high-quality filter
high-quality filter based on convolutions. It's name was supposed to reflect based on convolutions. It's name was supposed to reflect this. Starting from
this. Starting from Pillow 2.7.0 all resize method are based on convolutions. Pillow 2.7.0 all resize method are based on convolutions. All of them are
All of them are antialias from now on. And the real name of the antialias from now on. And the real name of the ``ANTIALIAS`` filter is Lanczos
:py:data:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. filter.
The :py:data:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility The ``ANTIALIAS`` constant is left for backward compatibility and is an alias
and is an alias for :py:data:`~PIL.Image.LANCZOS`. for ``LANCZOS``.
Lanczos upscaling quality Lanczos upscaling quality
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
The image upscaling quality with :py:data:`~PIL.Image.LANCZOS` filter was The image upscaling quality with ``LANCZOS`` filter was almost the same as
almost the same as :py:data:`~PIL.Image.BILINEAR` due to bug. This has been fixed. ``BILINEAR`` due to a bug. This has been fixed.
Bicubic upscaling quality Bicubic upscaling quality
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
The :py:data:`~PIL.Image.BICUBIC` filter for affine transformations produced The ``BICUBIC`` filter for affine transformations produced sharp, slightly
sharp, slightly pixelated image for upscaling. Bicubic for convolutions is pixelated image for upscaling. Bicubic for convolutions is more soft.
more soft.
Resize performance Resize performance
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
In most cases, convolution is more a expensive algorithm for downscaling In most cases, convolution is more a expensive algorithm for downscaling
because it takes into account all the pixels of source image. Therefore because it takes into account all the pixels of source image. Therefore
:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` filters' ``BILINEAR`` and ``BICUBIC`` filters' performance can be lower than before.
performance can be lower than before. On the other hand the quality of On the other hand the quality of ``BILINEAR`` and ``BICUBIC`` was close to
:py:data:`~PIL.Image.BILINEAR` and :py:data:`~PIL.Image.BICUBIC` was close to ``NEAREST``. So if such quality is suitable for your tasks you can switch to
:py:data:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks ``NEAREST`` filter for downscaling, which will give a huge improvement in
you can switch to :py:data:`~PIL.Image.NEAREST` filter for downscaling, performance.
which will give a huge improvement in performance.
At the same time performance of convolution resampling for downscaling has been At the same time performance of convolution resampling for downscaling has been
improved by around a factor of two compared to the previous version. improved by around a factor of two compared to the previous version.
The upscaling performance of the :py:data:`~PIL.Image.LANCZOS` filter has The upscaling performance of the ``LANCZOS`` filter has remained the same. For
remained the same. For :py:data:`~PIL.Image.BILINEAR` filter it has improved by ``BILINEAR`` filter it has improved by 1.5 times and for ``BICUBIC`` by four
1.5 times and for :py:data:`~PIL.Image.BICUBIC` by four times. times.
Default filter for thumbnails Default filter for thumbnails
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was
changed from :py:data:`~PIL.Image.NEAREST` to :py:data:`~PIL.Image.ANTIALIAS`. changed from ``NEAREST`` to ``ANTIALIAS``. Antialias was chosen because all the
Antialias was chosen because all the other filters gave poor quality for other filters gave poor quality for reduction. Starting from Pillow 2.7.0,
reduction. Starting from Pillow 2.7.0, :py:data:`~PIL.Image.ANTIALIAS` has been ``ANTIALIAS`` has been replaced with ``BICUBIC``, because it's faster and
replaced with :py:data:`~PIL.Image.BICUBIC`, because it's faster and ``ANTIALIAS`` doesn't give any advantages after downscaling with libjpeg, which
:py:data:`~PIL.Image.ANTIALIAS` doesn't give any advantages after uses supersampling internally, not convolutions.
downscaling with libjpeg, which uses supersampling internally, not convolutions.
Image transposition Image transposition
------------------- -------------------

View File

@ -53,6 +53,11 @@ Constants
A number of constants have been deprecated and will be removed in Pillow 10.0.0 A number of constants have been deprecated and will be removed in Pillow 10.0.0
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. (2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
.. note::
Some of these deprecations were restored in Pillow 9.4.0. See
:ref:`restored-image-constants`
===================================================== ============================================================ ===================================================== ============================================================
Deprecated Use instead Deprecated Use instead
===================================================== ============================================================ ===================================================== ============================================================

View File

@ -1,30 +1,6 @@
9.4.0 9.4.0
----- -----
Backwards Incompatible Changes
==============================
TODO
^^^^
TODO
Deprecations
============
TODO
^^^^
TODO
API Changes
===========
TODO
^^^^
TODO
API Additions API Additions
============= =============
@ -96,10 +72,21 @@ When saving a JPEG image, a comment can now be written from
Security Security
======== ========
TODO Fix memory DOS in ImageFont
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO A corrupt or specially crafted TTF font could have font metrics that lead to
unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not
check the image size before allocating memory for it. This dates to the PIL
fork. Pillow 8.2.0 added a check for large sizes, but did not consider the
case where one dimension is zero.
Null pointer dereference crash in ImageFont
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow attempted to dereference a null pointer in ``ImageFont``, leading to a
crash. An error is now raised instead. This has been present since
Pillow 8.0.0.
Other Changes Other Changes
============= =============
@ -109,3 +96,40 @@ Added support for DDS L and LA images
Support has been added to read and write L and LA DDS images in the uncompressed Support has been added to read and write L and LA DDS images in the uncompressed
format, known as "luminance" textures. format, known as "luminance" textures.
.. _restored-image-constants:
Constants
^^^^^^^^^
In Pillow 9.1.0, the following constants were deprecated. That has been reversed and
these constants will now remain available.
- ``Image.NONE``
- ``Image.NEAREST``
- ``Image.ORDERED``
- ``Image.RASTERIZE``
- ``Image.FLOYDSTEINBERG``
- ``Image.WEB``
- ``Image.ADAPTIVE``
- ``Image.AFFINE``
- ``Image.EXTENT``
- ``Image.PERSPECTIVE``
- ``Image.QUAD``
- ``Image.MESH``
- ``Image.FLIP_LEFT_RIGHT``
- ``Image.FLIP_TOP_BOTTOM``
- ``Image.ROTATE_90``
- ``Image.ROTATE_180``
- ``Image.ROTATE_270``
- ``Image.TRANSPOSE``
- ``Image.TRANSVERSE``
- ``Image.BOX``
- ``Image.BILINEAR``
- ``Image.HAMMING``
- ``Image.BICUBIC``
- ``Image.LANCZOS``
- ``Image.MEDIANCUT``
- ``Image.MAXCOVERAGE``
- ``Image.FASTOCTREE``
- ``Image.LIBIMAGEQUANT``

View File

@ -4,8 +4,8 @@ description = Python Imaging Library (Fork)
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown
url = https://python-pillow.org url = https://python-pillow.org
author = Alex Clark (PIL Fork Author) author = Jeffrey A. Clark (Alex)
author_email = aclark@python-pillow.org author_email = aclark@aclark.net
license = HPND license = HPND
classifiers = classifiers =
Development Status :: 6 - Mature Development Status :: 6 - Mature
@ -32,6 +32,7 @@ project_urls =
Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html
Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
Twitter=https://twitter.com/PythonPillow Twitter=https://twitter.com/PythonPillow
Mastodon=https://fosstodon.org/@pillow
[options] [options]
packages = PIL packages = PIL

View File

@ -263,18 +263,18 @@ def _pkg_config(name):
if not DEBUG: if not DEBUG:
command_libs.append("--silence-errors") command_libs.append("--silence-errors")
command_cflags.append("--silence-errors") command_cflags.append("--silence-errors")
libs = ( libs = re.split(
r"(^|\s+)-L",
subprocess.check_output(command_libs, stderr=stderr) subprocess.check_output(command_libs, stderr=stderr)
.decode("utf8") .decode("utf8")
.strip() .strip(),
.replace("-L", "") )[::2][1:]
) cflags = re.split(
cflags = ( r"(^|\s+)-I",
subprocess.check_output(command_cflags) subprocess.check_output(command_cflags, stderr=stderr)
.decode("utf8") .decode("utf8")
.strip() .strip(),
.replace("-I", "") )[::2][1:]
)
return libs, cflags return libs, cflags
except Exception: except Exception:
pass pass
@ -430,7 +430,6 @@ class pil_build_ext(build_ext):
return sdk_path return sdk_path
def build_extensions(self): def build_extensions(self):
library_dirs = [] library_dirs = []
include_dirs = [] include_dirs = []
@ -473,8 +472,12 @@ class pil_build_ext(build_ext):
else: else:
lib_root = include_root = root lib_root = include_root = root
_add_directory(library_dirs, lib_root) if lib_root is not None:
_add_directory(include_dirs, include_root) for lib_dir in lib_root:
_add_directory(library_dirs, lib_dir)
if include_root is not None:
for include_dir in include_root:
_add_directory(include_dirs, include_dir)
# respect CFLAGS/CPPFLAGS/LDFLAGS # respect CFLAGS/CPPFLAGS/LDFLAGS
for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"):
@ -913,7 +916,6 @@ class pil_build_ext(build_ext):
self.summary_report(feature) self.summary_report(feature)
def summary_report(self, feature): def summary_report(self, feature):
print("-" * 68) print("-" * 68)
print("PIL SETUP SUMMARY") print("PIL SETUP SUMMARY")
print("-" * 68) print("-" * 68)

View File

@ -226,7 +226,6 @@ class BmpImageFile(ImageFile.ImageFile):
# --------------- Once the header is processed, process the palette/LUT # --------------- Once the header is processed, process the palette/LUT
if self.mode == "P": # Paletted for 1, 4 and 8 bit images if self.mode == "P": # Paletted for 1, 4 and 8 bit images
# ---------------------------------------------------- 1-bit images # ---------------------------------------------------- 1-bit images
if not (0 < file_info["colors"] <= 65536): if not (0 < file_info["colors"] <= 65536):
msg = f"Unsupported BMP Palette size ({file_info['colors']})" msg = f"Unsupported BMP Palette size ({file_info['colors']})"
@ -363,7 +362,6 @@ class BmpRleDecoder(ImageFile.PyDecoder):
# Image plugin for the DIB format (BMP alias) # Image plugin for the DIB format (BMP alias)
# ============================================================================= # =============================================================================
class DibImageFile(BmpImageFile): class DibImageFile(BmpImageFile):
format = "DIB" format = "DIB"
format_description = "Windows Bitmap" format_description = "Windows Bitmap"

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