Merge branch 'main' into add-cygwin-to-ci

This commit is contained in:
Andrew Murray 2022-03-04 17:20:02 +11:00 committed by GitHub
commit e5537b545a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 2806 additions and 1386 deletions

View File

@ -12,7 +12,7 @@ environment:
matrix:
- PYTHON: C:/Python310
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python37-x64
ARCHITECTURE: x64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017

View File

@ -20,10 +20,12 @@ jobs:
arch,
centos-7-amd64,
centos-stream-8-amd64,
centos-stream-9-amd64,
debian-10-buster-x86,
debian-11-bullseye-x86,
fedora-34-amd64,
fedora-35-amd64,
gentoo,
ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
]

View File

@ -45,6 +45,7 @@ jobs:
${{ matrix.package }}-python-pyqt6 \
${{ matrix.package }}-python3-setuptools \
${{ matrix.package }}-freetype \
${{ matrix.package }}-gcc \
${{ matrix.package }}-ghostscript \
${{ matrix.package }}-lcms2 \
${{ matrix.package }}-libimagequant \

View File

@ -137,10 +137,11 @@ jobs:
& $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh
# failing with PyPy3
# skip PyPy for speed
- name: Enable heap verification
if: "!contains(matrix.python-version, 'pypy')"
run: "& 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\gflags.exe' /p /enable $env:pythonLocation\\python.exe"
run: |
& reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
- name: Test Pillow
run: |

View File

@ -91,7 +91,7 @@ jobs:
path: Tests/errors
- name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
run: |
python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
make doccheck

View File

@ -1,2 +1,8 @@
version: 2
python:
pip_install: true
install:
- method: pip
path: .
extra_requirements:
- docs

View File

@ -5,6 +5,60 @@ Changelog (Pillow)
9.1.0 (unreleased)
------------------
- Improved handling of PPM header #5121
[Piolie, radarhere]
- Reset size when seeking away from "Large Thumbnail" MPO frame #6101
[radarhere]
- Replace requirements.txt with extras #6072
[hugovk, radarhere]
- Added PyEncoder and support BLP saving #6069
[radarhere]
- Handle TGA images with packets that cross scan lines #6087
[radarhere]
- Added FITS reading #6056
[radarhere, hugovk]
- Added rawmode argument to Image.getpalette() #6061
[radarhere]
- Fixed BUFR, GRIB and HDF5 stub saving #6071
[radarhere]
- Do not automatically remove temporary ImageShow files on Unix #6045
[radarhere]
- Correctly read JPEG compressed BLP images #4685
[Meithal, radarhere]
- Merged _MODE_CONV typ into ImageMode as typestr #6057
[radarhere]
- Consider palette size when converting and in getpalette() #6060
[radarhere]
- Added enums #5954
[radarhere]
- Ensure image is opaque after converting P to PA with RGB palette #6052
[radarhere]
- Attach RGBA palettes from putpalette() when suitable #6054
[radarhere]
- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030
[radarhere]
- Drop excess values in BITSPERSAMPLE #6041
[mikhail-iurkov]
- Added unpacker from RGBA;15 to RGB #6031
[radarhere]
- Enable arm64 for MSVC on Windows #5811
[gaborkertesz-linaro, gaborkertesz]
@ -23,7 +77,7 @@ Changelog (Pillow)
- Ensure duplicated file pointer is closed #5946
[radarhere]
- Added specific error if ImagePath coordinate type is incorrect #5942
- Added specific error if path coordinate type is incorrect #5942
[radarhere]
- Return an empty bytestring from tobytes() for an empty image #5938

View File

@ -9,9 +9,11 @@ clean:
.PHONY: coverage
coverage:
pytest -qq
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq
rm -r htmlcov || true
coverage report
python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage
python3 -m coverage report
.PHONY: doc
doc:
@ -33,20 +35,16 @@ help:
@echo "Welcome to Pillow development. Please use \`make <target>\` where <target> is one of"
@echo " clean remove build products"
@echo " coverage run coverage test (in progress)"
@echo " doc make html docs"
@echo " docserve run an http server on the docs directory"
@echo " doc make HTML docs"
@echo " docserve run an HTTP server on the docs directory"
@echo " html to make standalone HTML files"
@echo " inplace make inplace extension"
@echo " install make and install"
@echo " install-coverage make and install with C coverage"
@echo " install-req install documentation and test dependencies"
@echo " install-venv (deprecated) install in virtualenv"
@echo " lint run the lint checks"
@echo " lint-fix run black and isort to (mostly) fix lint issues."
@echo " lint-fix run Black and isort to (mostly) fix lint issues"
@echo " release-test run code and package tests before release"
@echo " test run tests on installed pillow"
@echo " upload build and upload sdists to PyPI"
@echo " upload-test build and upload sdists to test.pythonpackages.com"
@echo " test run tests on installed Pillow"
.PHONY: inplace
inplace: clean
@ -70,28 +68,17 @@ debug:
make clean > /dev/null
CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null
.PHONY: install-req
install-req:
python3 -m pip install -r requirements.txt
.PHONY: install-venv
install-venv:
echo "'install-venv' is deprecated and will be removed in a future Pillow release"
virtualenv .
bin/pip install -r requirements.txt
.PHONY: release-test
release-test:
$(MAKE) install-req
python3 -m pip install -e .
python3 -m pip install -e .[tests]
python3 selftest.py
python3 -m pytest Tests
python3 -m pip install .
-rm dist/*.egg
-rmdir dist
python3 -m pytest -qq
check-manifest
pyroma .
python3 -m check-manifest
python3 -m pyroma .
$(MAKE) readme
.PHONY: sdist
@ -101,26 +88,30 @@ sdist:
.PHONY: test
test:
pytest -qq
python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest
python3 -m pytest -qq
.PHONY: valgrind
valgrind:
python3 -c "import pytest_valgrind" || python3 -m pip install pytest-valgrind
python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind
PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
--log-file=/tmp/valgrind-output \
python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
.PHONY: readme
readme:
markdown2 README.md > .long-description.html && open .long-description.html
python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2
python3 -m markdown2 README.md > .long-description.html && open .long-description.html
.PHONY: lint
lint:
tox --help > /dev/null || python3 -m pip install tox
tox -e lint
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
python3 -m tox -e lint
.PHONY: lint-fix
lint-fix:
black --target-version py37 .
isort .
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
python3 -m black --target-version py37 .
python3 -m isort .

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,5 @@
import os
import pytest
import warnings
from PIL import Image
@ -20,16 +19,14 @@ def test_bad():
either"""
for f in get_files("b"):
with pytest.warns(None) as record:
# Assert that there is no unclosed file warning
with warnings.catch_warnings():
try:
with Image.open(f) as im:
im.load()
except Exception: # as msg:
pass
# Assert that there is no unclosed file warning
assert not record
def test_questionable():
"""These shouldn't crash/dos, but it's not well defined that these

View File

@ -43,107 +43,158 @@ class TestColorLut3DCoreAPI:
im = Image.new("RGB", (10, 10), 0)
with pytest.raises(ValueError, match="filter"):
im.im.color_lut_3d("RGB", Image.CUBIC, *self.generate_identity_table(3, 3))
im.im.color_lut_3d(
"RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="image mode"):
im.im.color_lut_3d(
"wrong", Image.LINEAR, *self.generate_identity_table(3, 3)
"wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(5, 3))
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(1, 3))
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(2, 3))
with pytest.raises(ValueError, match="Table size"):
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (1, 3, 3))
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3)
)
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3)
)
with pytest.raises(ValueError, match="table_channels"):
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3)
)
with pytest.raises(ValueError, match="Table size"):
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (66, 3, 3))
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (1, 3, 3)),
)
with pytest.raises(ValueError, match="Table size"):
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (66, 3, 3)),
)
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 7)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7
)
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9
)
with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8
)
with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, 16)
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
def test_correct_args(self):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3))
im.im.color_lut_3d("CMYK", Image.LINEAR, *self.generate_identity_table(4, 3))
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 3, 3))
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (65, 3, 3))
"CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 65, 3))
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 3, 3)),
)
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 3, 65))
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (65, 3, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 65, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 3, 65)),
)
def test_wrong_mode(self):
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3))
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3))
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3))
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.LINEAR, *self.generate_identity_table(3, 3)
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(4, 3))
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_correct_mode(self):
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(3, 3))
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3))
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("HSV", Image.LINEAR, *self.generate_identity_table(3, 3))
im.im.color_lut_3d(
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3))
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_identities(self):
g = Image.linear_gradient("L")
im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
"RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
)
# Fast test with small cubes
@ -152,7 +203,9 @@ class TestColorLut3DCoreAPI:
im,
im._new(
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, size)
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, size),
)
),
)
@ -162,7 +215,9 @@ class TestColorLut3DCoreAPI:
im,
im._new(
im.im.color_lut_3d(
"RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 2, 65))
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 2, 65)),
)
),
)
@ -170,7 +225,12 @@ class TestColorLut3DCoreAPI:
def test_identities_4_channels(self):
g = Image.linear_gradient("L")
im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
"RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
)
# Red channel copied to alpha
@ -178,7 +238,9 @@ class TestColorLut3DCoreAPI:
Image.merge("RGBA", (im.split() * 2)[:4]),
im._new(
im.im.color_lut_3d(
"RGBA", Image.LINEAR, *self.generate_identity_table(4, 17)
"RGBA",
Image.Resampling.BILINEAR,
*self.generate_identity_table(4, 17),
)
),
)
@ -189,9 +251,9 @@ class TestColorLut3DCoreAPI:
"RGBA",
[
g,
g.transpose(Image.ROTATE_90),
g.transpose(Image.ROTATE_180),
g.transpose(Image.ROTATE_270),
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
g.transpose(Image.Transpose.ROTATE_270),
],
)
@ -199,7 +261,9 @@ class TestColorLut3DCoreAPI:
im,
im._new(
im.im.color_lut_3d(
"RGBA", Image.LINEAR, *self.generate_identity_table(3, 17)
"RGBA",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, 17),
)
),
)
@ -207,14 +271,19 @@ class TestColorLut3DCoreAPI:
def test_channels_order(self):
g = Image.linear_gradient("L")
im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
"RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
)
# Reverse channels by splitting and using table
# fmt: off
assert_image_equal(
Image.merge('RGB', im.split()[::-1]),
im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2, [
0, 0, 0, 0, 0, 1,
0, 1, 0, 0, 1, 1,
@ -227,11 +296,16 @@ class TestColorLut3DCoreAPI:
def test_overflow(self):
g = Image.linear_gradient("L")
im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
"RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
)
# fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2,
[
-1, -1, -1, 2, -1, -1,
@ -251,7 +325,7 @@ class TestColorLut3DCoreAPI:
assert transformed[205, 205] == (255, 255, 0)
# fmt: off
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
3, 2, 2, 2,
[
-3, -3, -3, 5, -3, -3,
@ -354,7 +428,12 @@ class TestColorLut3DFilter:
def test_numpy_formats(self):
g = Image.linear_gradient("L")
im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
"RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
)
lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b))
@ -445,7 +524,12 @@ class TestGenerateColorLut3D:
g = Image.linear_gradient("L")
im = Image.merge(
"RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)]
"RGB",
[
g,
g.transpose(Image.Transpose.ROTATE_90),
g.transpose(Image.Transpose.ROTATE_180),
],
)
assert im == im.filter(lut)

View File

@ -120,9 +120,9 @@ def test_apng_dispose_op_previous_frame():
# save_all=True,
# append_images=[green, blue],
# disposal=[
# PngImagePlugin.APNG_DISPOSE_OP_NONE,
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
# PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS
# PngImagePlugin.Disposal.OP_NONE,
# PngImagePlugin.Disposal.OP_PREVIOUS,
# PngImagePlugin.Disposal.OP_PREVIOUS
# ],
# )
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
@ -455,31 +455,31 @@ def test_apng_save_disposal(tmp_path):
green = Image.new("RGBA", size, (0, 255, 0, 255))
transparent = Image.new("RGBA", size, (0, 0, 0, 0))
# test APNG_DISPOSE_OP_NONE
# test OP_NONE
red.save(
test_file,
save_all=True,
append_images=[green, transparent],
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
disposal=PngImagePlugin.Disposal.OP_NONE,
blend=PngImagePlugin.Blend.OP_OVER,
)
with Image.open(test_file) as im:
im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_DISPOSE_OP_BACKGROUND
# test OP_BACKGROUND
disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND,
PngImagePlugin.APNG_DISPOSE_OP_NONE,
PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.Disposal.OP_BACKGROUND,
PngImagePlugin.Disposal.OP_NONE,
]
red.save(
test_file,
save_all=True,
append_images=[red, transparent],
disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
blend=PngImagePlugin.Blend.OP_OVER,
)
with Image.open(test_file) as im:
im.seek(2)
@ -487,26 +487,26 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND,
PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.Disposal.OP_BACKGROUND,
]
red.save(
test_file,
save_all=True,
append_images=[green],
disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
blend=PngImagePlugin.Blend.OP_OVER,
)
with Image.open(test_file) as im:
im.seek(1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_DISPOSE_OP_PREVIOUS
# test OP_PREVIOUS
disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
PngImagePlugin.APNG_DISPOSE_OP_NONE,
PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.Disposal.OP_PREVIOUS,
PngImagePlugin.Disposal.OP_NONE,
]
red.save(
test_file,
@ -514,7 +514,7 @@ def test_apng_save_disposal(tmp_path):
append_images=[green, red, transparent],
default_image=True,
disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
blend=PngImagePlugin.Blend.OP_OVER,
)
with Image.open(test_file) as im:
im.seek(3)
@ -522,15 +522,15 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
disposal = [
PngImagePlugin.APNG_DISPOSE_OP_NONE,
PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
PngImagePlugin.Disposal.OP_NONE,
PngImagePlugin.Disposal.OP_PREVIOUS,
]
red.save(
test_file,
save_all=True,
append_images=[green],
disposal=disposal,
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
blend=PngImagePlugin.Blend.OP_OVER,
)
with Image.open(test_file) as im:
im.seek(1)
@ -538,7 +538,7 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test info disposal
red.info["disposal"] = PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND
red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND
red.save(
test_file,
save_all=True,
@ -556,12 +556,12 @@ def test_apng_save_disposal_previous(tmp_path):
red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255))
# test APNG_DISPOSE_OP_NONE
# test OP_NONE
transparent.save(
test_file,
save_all=True,
append_images=[red, green],
disposal=PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS,
disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
)
with Image.open(test_file) as im:
im.seek(2)
@ -576,17 +576,17 @@ def test_apng_save_blend(tmp_path):
green = Image.new("RGBA", size, (0, 255, 0, 255))
transparent = Image.new("RGBA", size, (0, 0, 0, 0))
# test APNG_BLEND_OP_SOURCE on solid color
# test OP_SOURCE on solid color
blend = [
PngImagePlugin.APNG_BLEND_OP_OVER,
PngImagePlugin.APNG_BLEND_OP_SOURCE,
PngImagePlugin.Blend.OP_OVER,
PngImagePlugin.Blend.OP_SOURCE,
]
red.save(
test_file,
save_all=True,
append_images=[red, green],
default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
disposal=PngImagePlugin.Disposal.OP_NONE,
blend=blend,
)
with Image.open(test_file) as im:
@ -594,17 +594,17 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test APNG_BLEND_OP_SOURCE on transparent color
# test OP_SOURCE on transparent color
blend = [
PngImagePlugin.APNG_BLEND_OP_OVER,
PngImagePlugin.APNG_BLEND_OP_SOURCE,
PngImagePlugin.Blend.OP_OVER,
PngImagePlugin.Blend.OP_SOURCE,
]
red.save(
test_file,
save_all=True,
append_images=[red, transparent],
default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
disposal=PngImagePlugin.Disposal.OP_NONE,
blend=blend,
)
with Image.open(test_file) as im:
@ -612,14 +612,14 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
# test APNG_BLEND_OP_OVER
# test OP_OVER
red.save(
test_file,
save_all=True,
append_images=[green, transparent],
default_image=True,
disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE,
blend=PngImagePlugin.APNG_BLEND_OP_OVER,
disposal=PngImagePlugin.Disposal.OP_NONE,
blend=PngImagePlugin.Blend.OP_OVER,
)
with Image.open(test_file) as im:
im.seek(1)
@ -630,8 +630,18 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
# test info blend
red.info["blend"] = PngImagePlugin.APNG_BLEND_OP_OVER
red.info["blend"] = PngImagePlugin.Blend.OP_OVER
red.save(test_file, save_all=True, append_images=[green, transparent])
with Image.open(test_file) as im:
im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
def test_constants_deprecation():
for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_",
PngImagePlugin.Blend: "APNG_BLEND_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(PngImagePlugin, prefix + name) == enum[name]

View File

@ -1,8 +1,18 @@
import pytest
from PIL import Image
from PIL import BlpImagePlugin, Image
from .helper import assert_image_equal_tofile
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
hopper,
)
def test_load_blp1():
with Image.open("Tests/images/blp/blp1_jpeg.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
def test_load_blp2_raw():
@ -20,6 +30,28 @@ def test_load_blp2_dxt1a():
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
def test_save(tmp_path):
f = str(tmp_path / "temp.blp")
for version in ("BLP1", "BLP2"):
im = hopper("P")
im.save(f, blp_version=version)
with Image.open(f) as reloaded:
assert_image_equal(im.convert("RGB"), reloaded)
with Image.open("Tests/images/transparent.png") as im:
f = str(tmp_path / "temp.blp")
im.convert("P").save(f, blp_version=version)
with Image.open(f) as reloaded:
assert_image_similar(im, reloaded, 8)
im = hopper()
with pytest.raises(ValueError):
im.save(f)
@pytest.mark.parametrize(
"test_file",
[
@ -37,3 +69,14 @@ def test_crashes(test_file):
with Image.open(f) as im:
with pytest.raises(OSError):
im.load()
def test_constants_deprecation():
for enum, prefix in {
BlpImagePlugin.Format: "BLP_FORMAT_",
BlpImagePlugin.Encoding: "BLP_ENCODING_",
BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(BlpImagePlugin, prefix + name) == enum[name]

View File

@ -45,3 +45,35 @@ def test_save(tmp_path):
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
im.save(tmpfile)
def test_handler(tmp_path):
class TestHandler:
opened = False
loaded = False
saved = False
def open(self, im):
self.opened = True
def load(self, im):
self.loaded = True
return Image.new("RGB", (1, 1))
def save(self, im, fp, filename):
self.saved = True
handler = TestHandler()
BufrStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
im.load()
assert handler.loaded
temp_file = str(tmp_path / "temp.bufr")
im.save(temp_file)
assert handler.saved
BufrStubImagePlugin._handler = None

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import DcxImagePlugin, Image
@ -31,21 +33,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_FILE)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_FILE) as im:
im.load()
assert not record
def test_invalid_file():
with open("Tests/images/flower.jpg", "rb") as fp:

80
Tests/test_file_fits.py Normal file
View File

@ -0,0 +1,80 @@
from io import BytesIO
import pytest
from PIL import FitsImagePlugin, FitsStubImagePlugin, Image
from .helper import assert_image_equal, hopper
TEST_FILE = "Tests/images/hopper.fits"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
# Assert
assert im.format == "FITS"
assert im.size == (128, 128)
assert im.mode == "L"
assert_image_equal(im, hopper("L"))
def test_invalid_file():
# Arrange
invalid_file = "Tests/images/flower.jpg"
# Act / Assert
with pytest.raises(SyntaxError):
FitsImagePlugin.FitsImageFile(invalid_file)
def test_truncated_fits():
# No END to headers
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
with pytest.raises(OSError):
FitsImagePlugin.FitsImageFile(BytesIO(image_data))
def test_naxis_zero():
# This test image has been manually hexedited
# to set the number of data axes to zero
with pytest.raises(ValueError):
with Image.open("Tests/images/hopper_naxis_zero.fits"):
pass
def test_stub_deprecated():
class Handler:
opened = False
loaded = False
def open(self, im):
self.opened = True
def load(self, im):
self.loaded = True
return Image.new("RGB", (1, 1))
handler = Handler()
with pytest.warns(DeprecationWarning):
FitsStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert im.format == "FITS"
assert im.size == (128, 128)
assert im.mode == "L"
assert handler.opened
assert not handler.loaded
im.load()
assert handler.loaded
FitsStubImagePlugin._handler = None
Image.register_open(
FitsImagePlugin.FitsImageFile.format,
FitsImagePlugin.FitsImageFile,
FitsImagePlugin._accept,
)

View File

@ -1,63 +0,0 @@
from io import BytesIO
import pytest
from PIL import FitsStubImagePlugin, Image
TEST_FILE = "Tests/images/hopper.fits"
def test_open():
# Act
with Image.open(TEST_FILE) as im:
# Assert
assert im.format == "FITS"
assert im.size == (128, 128)
assert im.mode == "L"
def test_invalid_file():
# Arrange
invalid_file = "Tests/images/flower.jpg"
# Act / Assert
with pytest.raises(SyntaxError):
FitsStubImagePlugin.FITSStubImageFile(invalid_file)
def test_load():
# Arrange
with Image.open(TEST_FILE) as im:
# Act / Assert: stub cannot load without an implemented handler
with pytest.raises(OSError):
im.load()
def test_truncated_fits():
# No END to headers
image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
with pytest.raises(OSError):
FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data))
def test_naxis_zero():
# This test image has been manually hexedited
# to set the number of data axes to zero
with pytest.raises(ValueError):
with Image.open("Tests/images/hopper_naxis_zero.fits"):
pass
def test_save():
# Arrange
with Image.open(TEST_FILE) as im:
dummy_fp = None
dummy_filename = "dummy.filename"
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
im.save(dummy_filename)
with pytest.raises(OSError):
FitsStubImagePlugin._save(im, dummy_fp, dummy_filename)

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import FliImagePlugin, Image
@ -38,21 +40,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(static_test_file)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(static_test_file) as im:
im.load()
assert not record
def test_tell():
# Arrange

View File

@ -1,4 +1,6 @@
from PIL import Image
import pytest
from PIL import FtexImagePlugin, Image
from .helper import assert_image_equal_tofile, assert_image_similar
@ -12,3 +14,12 @@ def test_load_dxt1():
with Image.open("Tests/images/ftex_dxt1.ftc") as im:
with Image.open("Tests/images/ftex_dxt1.png") as target:
assert_image_similar(im, target.convert("RGBA"), 15)
def test_constants_deprecation():
for enum, prefix in {
FtexImagePlugin.Format: "FORMAT_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(FtexImagePlugin, prefix + name) == enum[name]

View File

@ -1,3 +1,4 @@
import warnings
from io import BytesIO
import pytest
@ -39,21 +40,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_GIF)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_GIF) as im:
im.load()
assert not record
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
@ -210,8 +207,8 @@ def test_palette_handling(tmp_path):
with Image.open(TEST_GIF) as im:
im = im.convert("RGB")
im = im.resize((100, 100), Image.LANCZOS)
im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256)
im = im.resize((100, 100), Image.Resampling.LANCZOS)
im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
f = str(tmp_path / "temp.gif")
im2.save(f, optimize=True)
@ -311,6 +308,22 @@ def test_n_frames():
assert im.is_animated == (n_frames != 1)
def test_no_change():
# Test n_frames does not change the image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(1)
expected = im.copy()
assert im.n_frames == 5
assert_image_equal(im, expected)
# Test is_animated does not change the image
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(3)
expected = im.copy()
assert im.is_animated
assert_image_equal(im, expected)
def test_eoferror():
with Image.open(TEST_GIF) as im:
n_frames = im.n_frames
@ -911,7 +924,7 @@ def test_save_I(tmp_path):
def test_getdata():
# Test getheader/getdata against legacy values.
# Create a 'P' image with holes in the palette.
im = Image._wedge().resize((16, 16), Image.NEAREST)
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0}

View File

@ -45,3 +45,35 @@ def test_save(tmp_path):
# Act / Assert: stub cannot save without an implemented handler
with pytest.raises(OSError):
im.save(tmpfile)
def test_handler(tmp_path):
class TestHandler:
opened = False
loaded = False
saved = False
def open(self, im):
self.opened = True
def load(self, im):
self.loaded = True
return Image.new("RGB", (1, 1))
def save(self, im, fp, filename):
self.saved = True
handler = TestHandler()
GribStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
im.load()
assert handler.loaded
temp_file = str(tmp_path / "temp.grib")
im.save(temp_file)
assert handler.saved
GribStubImagePlugin._handler = None

View File

@ -46,3 +46,35 @@ def test_save():
im.save(dummy_filename)
with pytest.raises(OSError):
Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename)
def test_handler(tmp_path):
class TestHandler:
opened = False
loaded = False
saved = False
def open(self, im):
self.opened = True
def load(self, im):
self.loaded = True
return Image.new("RGB", (1, 1))
def save(self, im, fp, filename):
self.saved = True
handler = TestHandler()
Hdf5StubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
im.load()
assert handler.loaded
temp_file = str(tmp_path / "temp.h5")
im.save(temp_file)
assert handler.saved
Hdf5StubImagePlugin._handler = None

View File

@ -1,5 +1,6 @@
import io
import os
import warnings
import pytest
@ -19,9 +20,8 @@ def test_sanity():
with Image.open(TEST_FILE) as im:
# Assert that there is no unclosed file warning
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.load()
assert not record
assert im.mode == "RGBA"
assert im.size == (1024, 1024)
@ -112,12 +112,9 @@ def test_older_icon():
def test_jp2_icon():
# This icon was made by using Uli Kusterer's oldiconutil to replace
# the PNG images with JPEG 2000 ones. The advantage of doing this is
# that OS X 10.5 supports JPEG 2000 but not PNG; some commercial
# software therefore does just this.
# (oldiconutil is here: https://github.com/uliwitness/oldiconutil)
# This icon uses JPEG 2000 images instead of the PNG images.
# The advantage of doing this is that OS X 10.5 supports JPEG 2000
# but not PNG; some commercial software therefore does just this.
if not ENABLE_JPEG2K:
return

View File

@ -53,7 +53,9 @@ def test_save_to_bytes():
assert im.mode == reloaded.mode
assert (64, 64) == reloaded.size
assert reloaded.format == "ICO"
assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS))
assert_image_equal(
reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS)
)
# The other one
output.seek(0)
@ -63,7 +65,9 @@ def test_save_to_bytes():
assert im.mode == reloaded.mode
assert (32, 32) == reloaded.size
assert reloaded.format == "ICO"
assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS))
assert_image_equal(
reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS)
)
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
@ -80,7 +84,7 @@ def test_save_to_bytes_bmp(mode):
assert "RGBA" == reloaded.mode
assert (64, 64) == reloaded.size
assert reloaded.format == "ICO"
im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA")
im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im)
# The other one
@ -91,7 +95,7 @@ def test_save_to_bytes_bmp(mode):
assert "RGBA" == reloaded.mode
assert (32, 32) == reloaded.size
assert reloaded.format == "ICO"
im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA")
im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im)

View File

@ -1,4 +1,5 @@
import filecmp
import warnings
import pytest
@ -35,21 +36,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_IM)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_IM) as im:
im.load()
assert not record
def test_tell():
# Arrange

View File

@ -1,5 +1,6 @@
import os
import re
import warnings
from io import BytesIO
import pytest
@ -271,7 +272,7 @@ class TestFileJpeg:
del exif[0x8769]
# Assert that it needs to be transposed
assert exif[0x0112] == Image.TRANSVERSE
assert exif[0x0112] == Image.Transpose.TRANSVERSE
# Assert that the GPS IFD is present and empty
assert exif.get_ifd(0x8825) == {}
@ -756,9 +757,8 @@ class TestFileJpeg:
assert exif[282] == 180
out = str(tmp_path / "out.jpg")
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.save(out, exif=exif)
assert not record
with Image.open(out) as reloaded:
assert reloaded.getexif()[282] == 180

View File

@ -291,7 +291,7 @@ def test_subsampling_decode(name):
# RGB reference images are downscaled
epsilon = 3e-3
width, height = width * 2, height * 2
expected = im2.resize((width, height), Image.NEAREST)
expected = im2.resize((width, height), Image.Resampling.NEAREST)
assert_image_similar(im, expected, epsilon)

View File

@ -112,7 +112,7 @@ class TestFileLibTiff(LibTiffTestCase):
test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig:
out = str(tmp_path / "temp.tif")
rot = orig.transpose(Image.ROTATE_90)
rot = orig.transpose(Image.Transpose.ROTATE_90)
assert rot.size == (500, 500)
rot.save(out)

View File

@ -1,3 +1,4 @@
import warnings
from io import BytesIO
import pytest
@ -41,21 +42,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(test_files[0])
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(test_files[0]) as im:
im.load()
assert not record
def test_app():
for test_file in test_files:
@ -88,6 +85,9 @@ def test_frame_size():
im.seek(1)
assert im.size == (680, 480)
im.seek(0)
assert im.size == (640, 480)
def test_ignore_frame_size():
# Ignore the different size of the second frame

View File

@ -1,5 +1,6 @@
import re
import sys
import warnings
import zlib
from io import BytesIO
@ -331,9 +332,8 @@ class TestFilePng:
with Image.open(TEST_PNG_FILE) as im:
# Assert that there is no unclosed file warning
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.verify()
assert not record
with Image.open(TEST_PNG_FILE) as im:
im.load()

View File

@ -3,7 +3,7 @@ from io import BytesIO
import pytest
from PIL import Image
from PIL import Image, UnidentifiedImageError
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -50,15 +50,70 @@ def test_pnm(tmp_path):
assert_image_equal_tofile(im, f)
def test_magic(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"PyInvalid")
with pytest.raises(UnidentifiedImageError):
with Image.open(path):
pass
def test_header_with_comments(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
with Image.open(path) as im:
assert im.size == (128, 128)
def test_non_integer_token(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\nTEST")
with pytest.raises(ValueError):
with Image.open(path):
pass
def test_token_too_long(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n 01234567890")
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Token too long in file header: b'01234567890'"
def test_too_many_colors(tmp_path):
path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f:
f.write(b"P6\n1 1\n1000\n")
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Too many colors for band: 1000"
def test_truncated_file(tmp_path):
path = str(tmp_path / "temp.pgm")
with open(path, "w") as f:
f.write("P6")
with pytest.raises(ValueError):
with pytest.raises(ValueError) as e:
with Image.open(path):
pass
assert str(e.value) == "Reached EOF while reading header"
def test_neg_ppm():
# Storage.c accepted negative values for xsize, ysize. the

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import Image, PsdImagePlugin
@ -29,21 +31,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(test_file)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(test_file) as im:
im.load()
assert not record
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"

View File

@ -1,4 +1,5 @@
import tempfile
import warnings
from io import BytesIO
import pytest
@ -28,21 +29,17 @@ def test_unclosed_file():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open(TEST_FILE)
im.load()
im.close()
assert not record
def test_context_manager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open(TEST_FILE) as im:
im.load()
assert not record
def test_save(tmp_path):
# Arrange

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import Image, TarIO, features
@ -31,16 +33,12 @@ def test_unclosed_file():
def test_close():
with pytest.warns(None) as record:
with warnings.catch_warnings():
tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
tar.close()
assert not record
def test_contextmanager():
with pytest.warns(None) as record:
with warnings.catch_warnings():
with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"):
pass
assert not record

View File

@ -97,6 +97,11 @@ def test_id_field_rle():
assert im.size == (199, 199)
def test_cross_scan_line():
with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga"
with Image.open(test_file) as im:

View File

@ -1,4 +1,5 @@
import os
import warnings
from io import BytesIO
import pytest
@ -64,20 +65,16 @@ class TestFileTiff:
pytest.warns(ResourceWarning, open)
def test_closed_file(self):
with pytest.warns(None) as record:
with warnings.catch_warnings():
im = Image.open("Tests/images/multipage.tiff")
im.load()
im.close()
assert not record
def test_context_manager(self):
with pytest.warns(None) as record:
with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im:
im.load()
assert not record
def test_mac_tiff(self):
# Read RGBa images from macOS [@PIL136]
@ -90,11 +87,18 @@ class TestFileTiff:
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
def test_wrong_bits_per_sample(self):
with Image.open("Tests/images/tiff_wrong_bits_per_sample.tiff") as im:
assert im.mode == "RGBA"
assert im.size == (52, 53)
assert im.tile == [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))]
@pytest.mark.parametrize(
"file_name,mode,size,offset",
[
("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160),
("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8),
],
)
def test_wrong_bits_per_sample(self, file_name, mode, size, offset):
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
assert im.size == size
assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))]
im.load()
def test_set_legacy_api(self):
@ -685,6 +689,32 @@ class TestFileTiff:
assert description[0]["format"] == "image/tiff"
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
def test_get_photoshop_blocks(self):
with Image.open("Tests/images/lab.tif") as im:
assert list(im.get_photoshop_blocks().keys()) == [
1061,
1002,
1005,
1062,
1037,
1049,
1011,
1034,
10000,
1013,
1016,
1032,
1054,
1050,
1064,
1041,
1044,
1036,
1057,
4000,
4001,
]
def test_close_on_load_exclusive(self, tmp_path):
# similar to test_fd_leak, but runs on unixlike os
tmpfile = str(tmp_path / "temp.tif")

View File

@ -1,6 +1,7 @@
import io
import re
import sys
import warnings
import pytest
@ -161,9 +162,8 @@ class TestFileWebp:
file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp")
with pytest.warns(None) as record:
with warnings.catch_warnings():
image.save(temp_file)
assert not record
def test_file_pointer_could_be_reused(self):
file_path = "Tests/images/hopper.webp"

View File

@ -77,7 +77,7 @@ def to_rgb_colorsys(im):
def test_wedge():
src = wedge().resize((3 * 32, 32), Image.BILINEAR)
src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR)
im = src.convert("HSV")
comparable = to_hsv_colorsys(src)

View File

@ -3,6 +3,7 @@ import os
import shutil
import sys
import tempfile
import warnings
import pytest
@ -648,9 +649,8 @@ class TestImage:
# Act/Assert
with Image.open(test_file) as im:
with pytest.warns(None) as record:
with warnings.catch_warnings():
im.save(temp_file)
assert not record
def test_load_on_nonexclusive_multiframe(self):
with open("Tests/images/frozenpond.mpo", "rb") as fp:
@ -813,6 +813,31 @@ class TestImage:
with pytest.warns(DeprecationWarning):
assert Image.CONTAINER == 2
def test_constants_deprecation(self):
with pytest.warns(DeprecationWarning):
assert Image.NEAREST == 0
with pytest.warns(DeprecationWarning):
assert Image.NONE == 0
with pytest.warns(DeprecationWarning):
assert Image.LINEAR == Image.Resampling.BILINEAR
with pytest.warns(DeprecationWarning):
assert Image.CUBIC == Image.Resampling.BICUBIC
with pytest.warns(DeprecationWarning):
assert Image.ANTIALIAS == Image.Resampling.LANCZOS
for enum in (
Image.Transpose,
Image.Transform,
Image.Resampling,
Image.Dither,
Image.Palette,
Image.Quantize,
):
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(Image, name) == enum[name]
@pytest.mark.parametrize(
"path",
[

View File

@ -76,6 +76,13 @@ def test_16bit_workaround():
_test_float_conversion(im.convert("I"))
def test_opaque():
alpha = hopper("P").convert("PA").getchannel("A")
solid = Image.new("L", (128, 128), 255)
assert_image_equal(alpha, solid)
def test_rgba_p():
im = hopper("RGBA")
im.putalpha(hopper("L"))
@ -136,7 +143,7 @@ def test_trns_l(tmp_path):
assert "transparency" in im_p.info
im_p.save(f)
im_p = im.convert("P", palette=Image.ADAPTIVE)
im_p = im.convert("P", palette=Image.Palette.ADAPTIVE)
assert "transparency" in im_p.info
im_p.save(f)
@ -159,13 +166,13 @@ def test_trns_RGB(tmp_path):
assert "transparency" not in im_rgba.info
im_rgba.save(f)
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE)
im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE)
assert "transparency" not in im_p.info
im_p.save(f)
im = Image.new("RGB", (1, 1))
im.info["transparency"] = im.getpixel((0, 0))
im_p = im.convert("P", palette=Image.ADAPTIVE)
im_p = im.convert("P", palette=Image.Palette.ADAPTIVE)
assert im_p.info["transparency"] == im_p.getpixel((0, 0))
im_p.save(f)

View File

@ -14,7 +14,7 @@ def test_sanity():
def test_roundtrip():
def getdata(mode):
im = hopper(mode).resize((32, 30), Image.NEAREST)
im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST)
data = im.getdata()
return data[0], len(data), len(list(data))

View File

@ -1,3 +1,5 @@
from PIL import Image
from .helper import hopper
@ -17,3 +19,26 @@ def test_palette():
assert palette("RGBA") is None
assert palette("CMYK") is None
assert palette("YCbCr") is None
def test_palette_rawmode():
im = Image.new("P", (1, 1))
im.putpalette((1, 2, 3))
for rawmode in ("RGB", None):
rgb = im.getpalette(rawmode)
assert rgb == [1, 2, 3]
# Convert the RGB palette to RGBA
rgba = im.getpalette("RGBA")
assert rgba == [1, 2, 3, 255]
im.putpalette((1, 2, 3, 4), "RGBA")
# Convert the RGBA palette to RGB
rgb = im.getpalette("RGB")
assert rgb == [1, 2, 3]
for rawmode in ("RGBA", None):
rgba = im.getpalette(rawmode)
assert rgba == [1, 2, 3, 4]

View File

@ -21,6 +21,7 @@ def test_sanity():
assert m.bands == ("1",)
assert m.basemode == "L"
assert m.basetype == "L"
assert m.typestr == "|b1"
for mode in (
"I;16",
@ -45,6 +46,7 @@ def test_sanity():
assert m.bands == ("R", "G", "B")
assert m.basemode == "RGB"
assert m.basetype == "L"
assert m.typestr == "|u1"
def test_properties():

View File

@ -45,7 +45,7 @@ class TestImagingPaste:
@cached_property
def mask_L(self):
return self.gradient_L.transpose(Image.ROTATE_270)
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@cached_property
def gradient_L(self):
@ -62,8 +62,8 @@ class TestImagingPaste:
"RGB",
[
self.gradient_L,
self.gradient_L.transpose(Image.ROTATE_90),
self.gradient_L.transpose(Image.ROTATE_180),
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
self.gradient_L.transpose(Image.Transpose.ROTATE_180),
],
)
@ -73,9 +73,9 @@ class TestImagingPaste:
"RGBA",
[
self.gradient_L,
self.gradient_L.transpose(Image.ROTATE_90),
self.gradient_L.transpose(Image.ROTATE_180),
self.gradient_L.transpose(Image.ROTATE_270),
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
self.gradient_L.transpose(Image.Transpose.ROTATE_180),
self.gradient_L.transpose(Image.Transpose.ROTATE_270),
],
)
@ -85,9 +85,9 @@ class TestImagingPaste:
"RGBa",
[
self.gradient_L,
self.gradient_L.transpose(Image.ROTATE_90),
self.gradient_L.transpose(Image.ROTATE_180),
self.gradient_L.transpose(Image.ROTATE_270),
self.gradient_L.transpose(Image.Transpose.ROTATE_90),
self.gradient_L.transpose(Image.Transpose.ROTATE_180),
self.gradient_L.transpose(Image.Transpose.ROTATE_270),
],
)

View File

@ -62,3 +62,17 @@ def test_putpalette_with_alpha_values():
im.putpalette(palette_with_alpha_values, "RGBA")
assert_image_equal(im.convert("RGBA"), expected)
@pytest.mark.parametrize(
"mode, palette",
(
("RGBA", (1, 2, 3, 4)),
("RGBAX", (1, 2, 3, 4, 0)),
),
)
def test_rgba_palette(mode, palette):
im = Image.new("P", (1, 1))
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]
assert im.palette.colors == {(1, 2, 3, 4): 0}

View File

@ -1,8 +1,9 @@
import pytest
from packaging.version import parse as parse_version
from PIL import Image
from PIL import Image, features
from .helper import assert_image_similar, hopper, is_ppc64le
from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature
def test_sanity():
@ -17,16 +18,14 @@ def test_sanity():
assert_image_similar(converted.convert("RGB"), image, 60)
@pytest.mark.xfail(is_ppc64le(), reason="failing on ppc64le on GHA")
@skip_unless_feature("libimagequant")
def test_libimagequant_quantize():
image = hopper()
try:
converted = image.quantize(100, Image.LIBIMAGEQUANT)
except ValueError as ex: # pragma: no cover
if "dependency" in str(ex).lower():
pytest.skip("libimagequant support not available")
else:
raise
if is_ppc64le():
libimagequant = parse_version(features.version_feature("libimagequant"))
if libimagequant < parse_version("4"):
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100
@ -34,7 +33,7 @@ def test_libimagequant_quantize():
def test_octree_quantize():
image = hopper()
converted = image.quantize(100, Image.FASTOCTREE)
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100
@ -61,7 +60,7 @@ def test_quantize_no_dither():
with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette = palette.convert("P")
converted = image.quantize(dither=0, palette=palette)
converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert converted.mode == "P"
assert converted.palette.palette == palette.palette.palette
@ -71,8 +70,8 @@ def test_quantize_dither_diff():
with Image.open("Tests/images/caption_6_33_22.png") as palette:
palette = palette.convert("P")
dither = image.quantize(dither=1, palette=palette)
nodither = image.quantize(dither=0, palette=palette)
dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette)
nodither = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert dither.tobytes() != nodither.tobytes()
@ -97,10 +96,10 @@ def test_transparent_colors_equal():
@pytest.mark.parametrize(
"method, color",
(
(Image.MEDIANCUT, (0, 0, 0)),
(Image.MAXCOVERAGE, (0, 0, 0)),
(Image.FASTOCTREE, (0, 0, 0)),
(Image.FASTOCTREE, (0, 0, 0, 0)),
(Image.Quantize.MEDIANCUT, (0, 0, 0)),
(Image.Quantize.MAXCOVERAGE, (0, 0, 0)),
(Image.Quantize.FASTOCTREE, (0, 0, 0)),
(Image.Quantize.FASTOCTREE, (0, 0, 0, 0)),
),
)
def test_palette(method, color):
@ -109,3 +108,18 @@ def test_palette(method, color):
converted = im.quantize(method=method)
converted_px = converted.load()
assert converted_px[0, 0] == converted.palette.colors[color]
def test_small_palette():
# Arrange
im = hopper()
colors = (255, 0, 0, 0, 0, 255)
p = Image.new("P", (1, 1))
p.putpalette(colors)
# Act
im = im.quantize(palette=p)
# Assert
assert len(im.getcolors()) == 2

View File

@ -97,7 +97,7 @@ def get_image(mode):
bands = [gradients_image]
for _ in mode_info.bands[1:]:
# rotate previous image
band = bands[-1].transpose(Image.ROTATE_90)
band = bands[-1].transpose(Image.Transpose.ROTATE_90)
bands.append(band)
# Correct alpha channel by transforming completely transparent pixels.
# Low alpha values also emphasize error after alpha multiplication.
@ -138,24 +138,26 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1):
reference = Image.new(im.mode, reduced.size)
area_size = (im.size[0] // factor[0], im.size[1] // factor[1])
area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1])
area = im.resize(area_size, Image.BOX, area_box)
area = im.resize(area_size, Image.Resampling.BOX, area_box)
reference.paste(area, (0, 0))
if area_size[0] < reduced.size[0]:
assert reduced.size[0] - area_size[0] == 1
last_column_box = (area_box[2], 0, im.size[0], area_box[3])
last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box)
last_column = im.resize(
(1, area_size[1]), Image.Resampling.BOX, last_column_box
)
reference.paste(last_column, (area_size[0], 0))
if area_size[1] < reduced.size[1]:
assert reduced.size[1] - area_size[1] == 1
last_row_box = (0, area_box[3], area_box[2], im.size[1])
last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box)
last_row = im.resize((area_size[0], 1), Image.Resampling.BOX, last_row_box)
reference.paste(last_row, (0, area_size[1]))
if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]:
last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1])
last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box)
last_pixel = im.resize((1, 1), Image.Resampling.BOX, last_pixel_box)
reference.paste(last_pixel, area_size)
assert_compare_images(reduced, reference, average_diff, max_diff)

View File

@ -24,7 +24,7 @@ class TestImagingResampleVulnerability:
):
with pytest.raises(MemoryError):
# any resampling filter will do here
im.im.resize((xsize, ysize), Image.BILINEAR)
im.im.resize((xsize, ysize), Image.Resampling.BILINEAR)
def test_invalid_size(self):
im = hopper()
@ -103,7 +103,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_box(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.BOX)
case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off
data = ("e1 e1"
"e1 e1")
@ -114,7 +114,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_bilinear(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.BILINEAR)
case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off
data = ("e1 c9"
"c9 b7")
@ -125,7 +125,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_hamming(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.HAMMING)
case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off
data = ("e1 da"
"da d3")
@ -136,7 +136,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_bicubic(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (12, 12), 0xE1)
case = case.resize((6, 6), Image.BICUBIC)
case = case.resize((6, 6), Image.Resampling.BICUBIC)
# fmt: off
data = ("e1 e3 d4"
"e3 e5 d6"
@ -148,7 +148,7 @@ class TestImagingCoreResampleAccuracy:
def test_reduce_lanczos(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (16, 16), 0xE1)
case = case.resize((8, 8), Image.LANCZOS)
case = case.resize((8, 8), Image.Resampling.LANCZOS)
# fmt: off
data = ("e1 e0 e4 d7"
"e0 df e3 d6"
@ -161,7 +161,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_box(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.BOX)
case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off
data = ("e1 e1"
"e1 e1")
@ -172,7 +172,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_bilinear(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.BILINEAR)
case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off
data = ("e1 b0"
"b0 98")
@ -183,7 +183,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_hamming(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.HAMMING)
case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off
data = ("e1 d2"
"d2 c5")
@ -194,7 +194,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_bicubic(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (4, 4), 0xE1)
case = case.resize((8, 8), Image.BICUBIC)
case = case.resize((8, 8), Image.Resampling.BICUBIC)
# fmt: off
data = ("e1 e5 ee b9"
"e5 e9 f3 bc"
@ -207,7 +207,7 @@ class TestImagingCoreResampleAccuracy:
def test_enlarge_lanczos(self):
for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (6, 6), 0xE1)
case = case.resize((12, 12), Image.LANCZOS)
case = case.resize((12, 12), Image.Resampling.LANCZOS)
data = (
"e1 e0 db ed f5 b8"
"e0 df da ec f3 b7"
@ -220,7 +220,9 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (12, 12)))
def test_box_filter_correct_range(self):
im = Image.new("RGB", (8, 8), "#1688ff").resize((100, 100), Image.BOX)
im = Image.new("RGB", (8, 8), "#1688ff").resize(
(100, 100), Image.Resampling.BOX
)
ref = Image.new("RGB", (100, 100), "#1688ff")
assert_image_equal(im, ref)
@ -228,7 +230,7 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency:
def make_case(self, mode, fill):
im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0]
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
def run_case(self, case):
channel, color = case
@ -283,20 +285,20 @@ class TestCoreResampleAlphaCorrect:
@pytest.mark.xfail(reason="Current implementation isn't precise enough")
def test_levels_rgba(self):
case = self.make_levels_case("RGBA")
self.run_levels_case(case.resize((512, 32), Image.BOX))
self.run_levels_case(case.resize((512, 32), Image.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS))
@pytest.mark.xfail(reason="Current implementation isn't precise enough")
def test_levels_la(self):
case = self.make_levels_case("LA")
self.run_levels_case(case.resize((512, 32), Image.BOX))
self.run_levels_case(case.resize((512, 32), Image.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.LANCZOS))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR))
self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING))
self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS))
def make_dirty_case(self, mode, clean_pixel, dirty_pixel):
i = Image.new(mode, (64, 64), dirty_pixel)
@ -321,19 +323,27 @@ class TestCoreResampleAlphaCorrect:
def test_dirty_pixels_rgba(self):
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0))
self.run_dirty_case(
case.resize((20, 20), Image.Resampling.BILINEAR), (255, 255, 0)
)
self.run_dirty_case(
case.resize((20, 20), Image.Resampling.HAMMING), (255, 255, 0)
)
self.run_dirty_case(
case.resize((20, 20), Image.Resampling.BICUBIC), (255, 255, 0)
)
self.run_dirty_case(
case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0)
)
def test_dirty_pixels_la(self):
case = self.make_dirty_case("LA", (255, 128), (0, 0))
self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,))
self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,))
self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,))
self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255,))
self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,))
self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,))
class TestCoreResamplePasses:
@ -346,26 +356,26 @@ class TestCoreResamplePasses:
def test_horizontal(self):
im = hopper("L")
with self.count(1):
im.resize((im.size[0] - 10, im.size[1]), Image.BILINEAR)
im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR)
def test_vertical(self):
im = hopper("L")
with self.count(1):
im.resize((im.size[0], im.size[1] - 10), Image.BILINEAR)
im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR)
def test_both(self):
im = hopper("L")
with self.count(2):
im.resize((im.size[0] - 10, im.size[1] - 10), Image.BILINEAR)
im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR)
def test_box_horizontal(self):
im = hopper("L")
box = (20, 0, im.size[0] - 20, im.size[1])
with self.count(1):
# the same size, but different box
with_box = im.resize(im.size, Image.BILINEAR, box)
with_box = im.resize(im.size, Image.Resampling.BILINEAR, box)
with self.count(2):
cropped = im.crop(box).resize(im.size, Image.BILINEAR)
cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR)
assert_image_similar(with_box, cropped, 0.1)
def test_box_vertical(self):
@ -373,9 +383,9 @@ class TestCoreResamplePasses:
box = (0, 20, im.size[0], im.size[1] - 20)
with self.count(1):
# the same size, but different box
with_box = im.resize(im.size, Image.BILINEAR, box)
with_box = im.resize(im.size, Image.Resampling.BILINEAR, box)
with self.count(2):
cropped = im.crop(box).resize(im.size, Image.BILINEAR)
cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR)
assert_image_similar(with_box, cropped, 0.1)
@ -388,7 +398,7 @@ class TestCoreResampleCoefficients:
draw = ImageDraw.Draw(i)
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
px = i.resize((5, i.size[1]), Image.BICUBIC).load()
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0]
@ -396,7 +406,7 @@ class TestCoreResampleCoefficients:
# regression test for the wrong coefficients calculation
# due to bug https://github.com/python-pillow/Pillow/issues/2161
im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF))
histogram = im.resize((256, 256), Image.BICUBIC).histogram()
histogram = im.resize((256, 256), Image.Resampling.BICUBIC).histogram()
# first channel
assert histogram[0x100 * 0 + 0x20] == 0x10000
@ -412,12 +422,12 @@ class TestCoreResampleBox:
def test_wrong_arguments(self):
im = hopper()
for resample in (
Image.NEAREST,
Image.BOX,
Image.BILINEAR,
Image.HAMMING,
Image.BICUBIC,
Image.LANCZOS,
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
):
im.resize((32, 32), resample, (0, 0, im.width, im.height))
im.resize((32, 32), resample, (20, 20, im.width, im.height))
@ -456,7 +466,7 @@ class TestCoreResampleBox:
for y0, y1 in split_range(dst_size[1], ytiles):
for x0, x1 in split_range(dst_size[0], xtiles):
box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1])
tile = im.resize((x1 - x0, y1 - y0), Image.BICUBIC, box)
tile = im.resize((x1 - x0, y1 - y0), Image.Resampling.BICUBIC, box)
tiled.paste(tile, (x0, y0))
return tiled
@ -467,7 +477,7 @@ class TestCoreResampleBox:
with Image.open("Tests/images/flower.jpg") as im:
assert im.size == (480, 360)
dst_size = (251, 188)
reference = im.resize(dst_size, Image.BICUBIC)
reference = im.resize(dst_size, Image.Resampling.BICUBIC)
for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]:
tiled = self.resize_tiled(im, dst_size, *tiles)
@ -483,12 +493,16 @@ class TestCoreResampleBox:
assert im.size == (480, 360)
dst_size = (48, 36)
# Reference is cropped image resized to destination
reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC)
# Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45)
supersampled = im.resize((60, 45), Image.BOX)
reference = im.crop((0, 0, 473, 353)).resize(
dst_size, Image.Resampling.BICUBIC
)
# Image.Resampling.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45)
supersampled = im.resize((60, 45), Image.Resampling.BOX)
with_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125))
without_box = supersampled.resize(dst_size, Image.BICUBIC)
with_box = supersampled.resize(
dst_size, Image.Resampling.BICUBIC, (0, 0, 59.125, 44.125)
)
without_box = supersampled.resize(dst_size, Image.Resampling.BICUBIC)
# error with box should be much smaller than without
assert_image_similar(reference, with_box, 6)
@ -496,7 +510,7 @@ class TestCoreResampleBox:
assert_image_similar(reference, without_box, 5)
def test_formats(self):
for resample in [Image.NEAREST, Image.BILINEAR]:
for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]:
for mode in ["RGB", "L", "RGBA", "LA", "I", ""]:
im = hopper(mode)
box = (20, 20, im.size[0] - 20, im.size[1] - 20)
@ -514,7 +528,7 @@ class TestCoreResampleBox:
((40, 50), (10, 0, 50, 50)),
((40, 50), (10, 20, 50, 70)),
]:
res = im.resize(size, Image.LANCZOS, box)
res = im.resize(size, Image.Resampling.LANCZOS, box)
assert res.size == size
assert_image_equal(res, im.crop(box), f">>> {size} {box}")
@ -528,7 +542,7 @@ class TestCoreResampleBox:
((40, 50), (10.4, 0.4, 50.4, 50.4)),
((40, 50), (10.4, 20.4, 50.4, 70.4)),
]:
res = im.resize(size, Image.LANCZOS, box)
res = im.resize(size, Image.Resampling.LANCZOS, box)
assert res.size == size
with pytest.raises(AssertionError, match=r"difference \d"):
# check that the difference at least that much
@ -538,7 +552,7 @@ class TestCoreResampleBox:
# Can skip resize for one dimension
im = hopper()
for flt in [Image.NEAREST, Image.BICUBIC]:
for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
for size, box in [
((40, 50), (0, 0, 40, 90)),
((40, 50), (0, 20, 40, 90)),
@ -559,7 +573,7 @@ class TestCoreResampleBox:
# Can skip resize for one dimension
im = hopper()
for flt in [Image.NEAREST, Image.BICUBIC]:
for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
for size, box in [
((40, 50), (0, 0, 90, 50)),
((40, 50), (20, 0, 90, 50)),

View File

@ -35,33 +35,33 @@ class TestImagingCoreResize:
"I;16",
]: # exotic mode
im = hopper(mode)
r = self.resize(im, (15, 12), Image.NEAREST)
r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
assert r.mode == mode
assert r.size == (15, 12)
assert r.im.bands == im.im.bands
def test_convolution_modes(self):
with pytest.raises(ValueError):
self.resize(hopper("1"), (15, 12), Image.BILINEAR)
self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR)
with pytest.raises(ValueError):
self.resize(hopper("P"), (15, 12), Image.BILINEAR)
self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR)
with pytest.raises(ValueError):
self.resize(hopper("I;16"), (15, 12), Image.BILINEAR)
self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR)
for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]:
im = hopper(mode)
r = self.resize(im, (15, 12), Image.BILINEAR)
r = self.resize(im, (15, 12), Image.Resampling.BILINEAR)
assert r.mode == mode
assert r.size == (15, 12)
assert r.im.bands == im.im.bands
def test_reduce_filters(self):
for f in [
Image.NEAREST,
Image.BOX,
Image.BILINEAR,
Image.HAMMING,
Image.BICUBIC,
Image.LANCZOS,
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
]:
r = self.resize(hopper("RGB"), (15, 12), f)
assert r.mode == "RGB"
@ -69,12 +69,12 @@ class TestImagingCoreResize:
def test_enlarge_filters(self):
for f in [
Image.NEAREST,
Image.BOX,
Image.BILINEAR,
Image.HAMMING,
Image.BICUBIC,
Image.LANCZOS,
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
]:
r = self.resize(hopper("RGB"), (212, 195), f)
assert r.mode == "RGB"
@ -95,12 +95,12 @@ class TestImagingCoreResize:
samples["dirty"].putpixel((1, 1), 128)
for f in [
Image.NEAREST,
Image.BOX,
Image.BILINEAR,
Image.HAMMING,
Image.BICUBIC,
Image.LANCZOS,
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
]:
# samples resized with current filter
references = {
@ -124,12 +124,12 @@ class TestImagingCoreResize:
def test_enlarge_zero(self):
for f in [
Image.NEAREST,
Image.BOX,
Image.BILINEAR,
Image.HAMMING,
Image.BICUBIC,
Image.LANCZOS,
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
]:
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f)
assert r.mode == "RGB"
@ -164,15 +164,19 @@ def gradients_image():
class TestReducingGapResize:
def test_reducing_gap_values(self, gradients_image):
ref = gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None)
im = gradients_image.resize((52, 34), Image.BICUBIC)
ref = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, reducing_gap=None
)
im = gradients_image.resize((52, 34), Image.Resampling.BICUBIC)
assert_image_equal(ref, im)
with pytest.raises(ValueError):
gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0)
gradients_image.resize((52, 34), Image.Resampling.BICUBIC, reducing_gap=0)
with pytest.raises(ValueError):
gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99)
gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99
)
def test_reducing_gap_1(self, gradients_image):
for box, epsilon in [
@ -180,9 +184,9 @@ class TestReducingGapResize:
((1.1, 2.2, 510.8, 510.9), 4),
((3, 10, 410, 256), 10),
]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=1.0
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
)
with pytest.raises(AssertionError):
@ -196,9 +200,9 @@ class TestReducingGapResize:
((1.1, 2.2, 510.8, 510.9), 1.5),
((3, 10, 410, 256), 1),
]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=2.0
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
)
with pytest.raises(AssertionError):
@ -212,9 +216,9 @@ class TestReducingGapResize:
((1.1, 2.2, 510.8, 510.9), 1),
((3, 10, 410, 256), 0.5),
]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=3.0
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
)
with pytest.raises(AssertionError):
@ -224,9 +228,9 @@ class TestReducingGapResize:
def test_reducing_gap_8(self, gradients_image):
for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]:
ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=8.0
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
)
assert_image_equal(ref, im)
@ -236,8 +240,10 @@ class TestReducingGapResize:
((0, 0, 512, 512), 5.5),
((0.9, 1.7, 128, 128), 9.5),
]:
ref = gradients_image.resize((52, 34), Image.BOX, box=box)
im = gradients_image.resize((52, 34), Image.BOX, box=box, reducing_gap=1.0)
ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
im = gradients_image.resize(
(52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
)
assert_image_similar(ref, im, epsilon)
@ -261,12 +267,12 @@ class TestImageResize:
def test_default_filter(self):
for mode in "L", "RGB", "I", "F":
im = hopper(mode)
assert im.resize((20, 20), Image.BICUBIC) == im.resize((20, 20))
assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
for mode in "1", "P":
im = hopper(mode)
assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20))
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))
for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16":
im = hopper(mode)
assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20))
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))

View File

@ -46,14 +46,14 @@ def test_zero():
def test_resample():
# Target image creation, inspected by eye.
# >>> im = Image.open('Tests/images/hopper.ppm')
# >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True)
# >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True)
# >>> im.save('Tests/images/hopper_45.png')
with Image.open("Tests/images/hopper_45.png") as target:
for (resample, epsilon) in (
(Image.NEAREST, 10),
(Image.BILINEAR, 5),
(Image.BICUBIC, 0),
(Image.Resampling.NEAREST, 10),
(Image.Resampling.BILINEAR, 5),
(Image.Resampling.BICUBIC, 0),
):
im = hopper()
im = im.rotate(45, resample=resample, expand=True)
@ -62,7 +62,7 @@ def test_resample():
def test_center_0():
im = hopper()
im = im.rotate(45, center=(0, 0), resample=Image.BICUBIC)
im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2
@ -73,7 +73,7 @@ def test_center_0():
def test_center_14():
im = hopper()
im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC)
im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper_45.png") as target:
target_origin = target.size[1] / 2 - 14
@ -90,7 +90,7 @@ def test_translate():
(target_origin, target_origin, target_origin + 128, target_origin + 128)
)
im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC)
im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC)
assert_image_similar(im, target, 1)

View File

@ -97,24 +97,24 @@ def test_DCT_scaling_edges():
thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0))
# small reducing_gap to amplify the effect
thumb.thumbnail((32, 32), Image.BICUBIC, reducing_gap=1.0)
thumb.thumbnail((32, 32), Image.Resampling.BICUBIC, reducing_gap=1.0)
ref = im.resize((32, 32), Image.BICUBIC)
ref = im.resize((32, 32), Image.Resampling.BICUBIC)
# This is still JPEG, some error is present. Without the fix it is 11.5
assert_image_similar(thumb, ref, 1.5)
def test_reducing_gap_values():
im = hopper()
im.thumbnail((18, 18), Image.BICUBIC)
im.thumbnail((18, 18), Image.Resampling.BICUBIC)
ref = hopper()
ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=2.0)
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=2.0)
# reducing_gap=2.0 should be the default
assert_image_equal(ref, im)
ref = hopper()
ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=None)
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
with pytest.raises(AssertionError):
assert_image_equal(ref, im)
@ -125,9 +125,9 @@ def test_reducing_gap_for_DCT_scaling():
with Image.open("Tests/images/hopper.jpg") as ref:
# thumbnail should call draft with reducing_gap scale
ref.draft(None, (18 * 3, 18 * 3))
ref = ref.resize((18, 18), Image.BICUBIC)
ref = ref.resize((18, 18), Image.Resampling.BICUBIC)
with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((18, 18), Image.BICUBIC, reducing_gap=3.0)
im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
assert_image_equal(ref, im)

View File

@ -34,20 +34,22 @@ class TestImageTransform:
def test_palette(self):
with Image.open("Tests/images/hopper.gif") as im:
transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0])
transformed = im.transform(
im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0]
)
assert im.palette.palette == transformed.palette.palette
def test_extent(self):
im = hopper("RGB")
(w, h) = im.size
# fmt: off
transformed = im.transform(im.size, Image.EXTENT,
transformed = im.transform(im.size, Image.Transform.EXTENT,
(0, 0,
w//2, h//2), # ul -> lr
Image.BILINEAR)
Image.Resampling.BILINEAR)
# fmt: on
scaled = im.resize((w * 2, h * 2), Image.BILINEAR).crop((0, 0, w, h))
scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h))
# undone -- precision?
assert_image_similar(transformed, scaled, 23)
@ -57,15 +59,18 @@ class TestImageTransform:
im = hopper("RGB")
(w, h) = im.size
# fmt: off
transformed = im.transform(im.size, Image.QUAD,
transformed = im.transform(im.size, Image.Transform.QUAD,
(0, 0, 0, h//2,
# ul -> ccw around quad:
w//2, h//2, w//2, 0),
Image.BILINEAR)
Image.Resampling.BILINEAR)
# fmt: on
scaled = im.transform(
(w, h), Image.AFFINE, (0.5, 0, 0, 0, 0.5, 0), Image.BILINEAR
(w, h),
Image.Transform.AFFINE,
(0.5, 0, 0, 0, 0.5, 0),
Image.Resampling.BILINEAR,
)
assert_image_equal(transformed, scaled)
@ -80,9 +85,9 @@ class TestImageTransform:
(w, h) = im.size
transformed = im.transform(
im.size,
Image.EXTENT,
Image.Transform.EXTENT,
(0, 0, w * 2, h * 2),
Image.BILINEAR,
Image.Resampling.BILINEAR,
fillcolor="red",
)
@ -93,18 +98,21 @@ class TestImageTransform:
im = hopper("RGBA")
(w, h) = im.size
# fmt: off
transformed = im.transform(im.size, Image.MESH,
transformed = im.transform(im.size, Image.Transform.MESH,
[((0, 0, w//2, h//2), # box
(0, 0, 0, h,
w, h, w, 0)), # ul -> ccw around quad
((w//2, h//2, w, h), # box
(0, 0, 0, h,
w, h, w, 0))], # ul -> ccw around quad
Image.BILINEAR)
Image.Resampling.BILINEAR)
# fmt: on
scaled = im.transform(
(w // 2, h // 2), Image.AFFINE, (2, 0, 0, 0, 2, 0), Image.BILINEAR
(w // 2, h // 2),
Image.Transform.AFFINE,
(2, 0, 0, 0, 2, 0),
Image.Resampling.BILINEAR,
)
checker = Image.new("RGBA", im.size)
@ -137,14 +145,16 @@ class TestImageTransform:
def test_alpha_premult_resize(self):
def op(im, sz):
return im.resize(sz, Image.BILINEAR)
return im.resize(sz, Image.Resampling.BILINEAR)
self._test_alpha_premult(op)
def test_alpha_premult_transform(self):
def op(im, sz):
(w, h) = im.size
return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.BILINEAR)
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
)
self._test_alpha_premult(op)
@ -171,7 +181,7 @@ class TestImageTransform:
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_resize(self, mode):
def op(im, sz):
return im.resize(sz, Image.NEAREST)
return im.resize(sz, Image.Resampling.NEAREST)
self._test_nearest(op, mode)
@ -179,7 +189,9 @@ class TestImageTransform:
def test_nearest_transform(self, mode):
def op(im, sz):
(w, h) = im.size
return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.NEAREST)
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
)
self._test_nearest(op, mode)
@ -213,13 +225,15 @@ class TestImageTransform:
def test_unknown_resampling_filter(self):
with hopper() as im:
(w, h) = im.size
for resample in (Image.BOX, "unknown"):
for resample in (Image.Resampling.BOX, "unknown"):
with pytest.raises(ValueError):
im.transform((100, 100), Image.EXTENT, (0, 0, w, h), resample)
im.transform(
(100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample
)
class TestImageTransformAffine:
transform = Image.AFFINE
transform = Image.Transform.AFFINE
def _test_image(self):
im = hopper("RGB")
@ -247,7 +261,11 @@ class TestImageTransformAffine:
else:
transposed = im
for resample in [Image.NEAREST, Image.BILINEAR, Image.BICUBIC]:
for resample in [
Image.Resampling.NEAREST,
Image.Resampling.BILINEAR,
Image.Resampling.BICUBIC,
]:
transformed = im.transform(
transposed.size, self.transform, matrix, resample
)
@ -257,13 +275,13 @@ class TestImageTransformAffine:
self._test_rotate(0, None)
def test_rotate_90_deg(self):
self._test_rotate(90, Image.ROTATE_90)
self._test_rotate(90, Image.Transpose.ROTATE_90)
def test_rotate_180_deg(self):
self._test_rotate(180, Image.ROTATE_180)
self._test_rotate(180, Image.Transpose.ROTATE_180)
def test_rotate_270_deg(self):
self._test_rotate(270, Image.ROTATE_270)
self._test_rotate(270, Image.Transpose.ROTATE_270)
def _test_resize(self, scale, epsilonscale):
im = self._test_image()
@ -273,9 +291,9 @@ class TestImageTransformAffine:
matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0]
for resample, epsilon in [
(Image.NEAREST, 0),
(Image.BILINEAR, 2),
(Image.BICUBIC, 1),
(Image.Resampling.NEAREST, 0),
(Image.Resampling.BILINEAR, 2),
(Image.Resampling.BICUBIC, 1),
]:
transformed = im.transform(size_up, self.transform, matrix_up, resample)
transformed = transformed.transform(
@ -306,9 +324,9 @@ class TestImageTransformAffine:
matrix_down = [1, 0, x, 0, 1, y, 0, 0]
for resample, epsilon in [
(Image.NEAREST, 0),
(Image.BILINEAR, 1.5),
(Image.BICUBIC, 1),
(Image.Resampling.NEAREST, 0),
(Image.Resampling.BILINEAR, 1.5),
(Image.Resampling.BICUBIC, 1),
]:
transformed = im.transform(size_up, self.transform, matrix_up, resample)
transformed = transformed.transform(
@ -328,4 +346,4 @@ class TestImageTransformAffine:
class TestImageTransformPerspective(TestImageTransformAffine):
# Repeat all tests for AFFINE transformations with PERSPECTIVE
transform = Image.PERSPECTIVE
transform = Image.Transform.PERSPECTIVE

View File

@ -1,12 +1,4 @@
from PIL.Image import (
FLIP_LEFT_RIGHT,
FLIP_TOP_BOTTOM,
ROTATE_90,
ROTATE_180,
ROTATE_270,
TRANSPOSE,
TRANSVERSE,
)
from PIL.Image import Transpose
from . import helper
from .helper import assert_image_equal
@ -20,7 +12,7 @@ HOPPER = {
def test_flip_left_right():
def transpose(mode):
im = HOPPER[mode]
out = im.transpose(FLIP_LEFT_RIGHT)
out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
assert out.mode == mode
assert out.size == im.size
@ -37,7 +29,7 @@ def test_flip_left_right():
def test_flip_top_bottom():
def transpose(mode):
im = HOPPER[mode]
out = im.transpose(FLIP_TOP_BOTTOM)
out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
assert out.mode == mode
assert out.size == im.size
@ -54,7 +46,7 @@ def test_flip_top_bottom():
def test_rotate_90():
def transpose(mode):
im = HOPPER[mode]
out = im.transpose(ROTATE_90)
out = im.transpose(Transpose.ROTATE_90)
assert out.mode == mode
assert out.size == im.size[::-1]
@ -71,7 +63,7 @@ def test_rotate_90():
def test_rotate_180():
def transpose(mode):
im = HOPPER[mode]
out = im.transpose(ROTATE_180)
out = im.transpose(Transpose.ROTATE_180)
assert out.mode == mode
assert out.size == im.size
@ -88,7 +80,7 @@ def test_rotate_180():
def test_rotate_270():
def transpose(mode):
im = HOPPER[mode]
out = im.transpose(ROTATE_270)
out = im.transpose(Transpose.ROTATE_270)
assert out.mode == mode
assert out.size == im.size[::-1]
@ -105,7 +97,7 @@ def test_rotate_270():
def test_transpose():
def transpose(mode):
im = HOPPER[mode]
out = im.transpose(TRANSPOSE)
out = im.transpose(Transpose.TRANSPOSE)
assert out.mode == mode
assert out.size == im.size[::-1]
@ -122,7 +114,7 @@ def test_transpose():
def test_tranverse():
def transpose(mode):
im = HOPPER[mode]
out = im.transpose(TRANSVERSE)
out = im.transpose(Transpose.TRANSVERSE)
assert out.mode == mode
assert out.size == im.size[::-1]
@ -143,20 +135,31 @@ def test_roundtrip():
def transpose(first, second):
return im.transpose(first).transpose(second)
assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT))
assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM))
assert_image_equal(im, transpose(ROTATE_90, ROTATE_270))
assert_image_equal(im, transpose(ROTATE_180, ROTATE_180))
assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM)
im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT)
)
assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT)
im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM)
)
assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270))
assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180))
assert_image_equal(
im.transpose(Transpose.TRANSPOSE),
transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM),
)
assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT)
im.transpose(Transpose.TRANSPOSE),
transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT),
)
assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM)
im.transpose(Transpose.TRANSVERSE),
transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT),
)
assert_image_equal(
im.transpose(Transpose.TRANSVERSE),
transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM),
)
assert_image_equal(
im.transpose(Transpose.TRANSVERSE),
transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE),
)
assert_image_equal(im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE))

View File

@ -140,7 +140,7 @@ def test_intent():
skip_missing()
assert ImageCms.getDefaultIntent(SRGB) == 0
support = ImageCms.isIntentSupported(
SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT
SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT
)
assert support == 1
@ -153,7 +153,7 @@ def test_profile_object():
# ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""]
assert ImageCms.getDefaultIntent(p) == 0
support = ImageCms.isIntentSupported(
p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT
p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT
)
assert support == 1
@ -593,3 +593,13 @@ def test_auxiliary_channels_isolated():
)
assert_image_equal(test_image.convert(dst_format[2]), reference_image)
def test_constants_deprecation():
for enum, prefix in {
ImageCms.Intent: "INTENT_",
ImageCms.Direction: "DIRECTION_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(ImageCms, prefix + name) == enum[name]

View File

@ -183,7 +183,7 @@ def test_bitmap():
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
with Image.open("Tests/images/pil123rgba.png") as small:
small = small.resize((50, 50), Image.NEAREST)
small = small.resize((50, 50), Image.Resampling.NEAREST)
# Act
draw.bitmap((10, 10), small)
@ -319,7 +319,7 @@ def test_ellipse_symmetric():
im = Image.new("RGB", (width, 100))
draw = ImageDraw.Draw(im)
draw.ellipse(bbox, fill="green", outline="blue")
assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT))
assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT))
def test_ellipse_width():

View File

@ -23,7 +23,7 @@ class TestImageFile:
def test_parser(self):
def roundtrip(format):
im = hopper("L").resize((1000, 1000), Image.NEAREST)
im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST)
if format in ("MSP", "XBM"):
im = im.convert("1")
@ -124,6 +124,23 @@ class TestImageFile:
with pytest.raises(OSError):
p.close()
def test_no_format(self):
buf = BytesIO(b"\x00" * 255)
class DummyImageFile(ImageFile.ImageFile):
def _open(self):
self.mode = "RGB"
self._size = (1, 1)
im = DummyImageFile(buf)
assert im.format is None
assert im.get_format_mimetype() is None
def test_oserror(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
def test_truncated(self):
b = BytesIO(
b"BM000000000000" # head_data
@ -179,6 +196,11 @@ class MockPyDecoder(ImageFile.PyDecoder):
return -1, 0
class MockPyEncoder(ImageFile.PyEncoder):
def encode(self, buffer):
return 1, 1, b""
xoff, yoff, xsize, ysize = 10, 20, 100, 100
@ -190,53 +212,58 @@ class MockImageFile(ImageFile.ImageFile):
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
class TestPyDecoder:
def get_decoder(self):
decoder = MockPyDecoder(None)
class CodecsTest:
@classmethod
def setup_class(cls):
cls.decoder = MockPyDecoder(None)
cls.encoder = MockPyEncoder(None)
def closure(mode, *args):
decoder.__init__(mode, *args)
return decoder
def decoder_closure(mode, *args):
cls.decoder.__init__(mode, *args)
return cls.decoder
Image.register_decoder("MOCK", closure)
return decoder
def encoder_closure(mode, *args):
cls.encoder.__init__(mode, *args)
return cls.encoder
Image.register_decoder("MOCK", decoder_closure)
Image.register_encoder("MOCK", encoder_closure)
class TestPyDecoder(CodecsTest):
def test_setimage(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
d = self.get_decoder()
im.load()
assert d.state.xoff == xoff
assert d.state.yoff == yoff
assert d.state.xsize == xsize
assert d.state.ysize == ysize
assert self.decoder.state.xoff == xoff
assert self.decoder.state.yoff == yoff
assert self.decoder.state.xsize == xsize
assert self.decoder.state.ysize == ysize
with pytest.raises(ValueError):
d.set_as_raw(b"\x00")
self.decoder.set_as_raw(b"\x00")
def test_extents_none(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
d = self.get_decoder()
im.load()
assert d.state.xoff == 0
assert d.state.yoff == 0
assert d.state.xsize == 200
assert d.state.ysize == 200
assert self.decoder.state.xoff == 0
assert self.decoder.state.yoff == 0
assert self.decoder.state.xsize == 200
assert self.decoder.state.ysize == 200
def test_negsize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError):
im.load()
@ -250,7 +277,6 @@ class TestPyDecoder:
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
self.get_decoder()
with pytest.raises(ValueError):
im.load()
@ -259,14 +285,90 @@ class TestPyDecoder:
with pytest.raises(ValueError):
im.load()
def test_no_format(self):
def test_decode(self):
decoder = ImageFile.PyDecoder(None)
with pytest.raises(NotImplementedError):
decoder.decode(None)
class TestPyEncoder(CodecsTest):
def test_setimage(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
assert im.format is None
assert im.get_format_mimetype() is None
def test_oserror(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
im.save(BytesIO(), "JPEG2000", num_resolutions=2)
fp = BytesIO()
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
)
assert self.encoder.state.xoff == xoff
assert self.encoder.state.yoff == yoff
assert self.encoder.state.xsize == xsize
assert self.encoder.state.ysize == ysize
def test_extents_none(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
assert self.encoder.state.xoff == 0
assert self.encoder.state.yoff == 0
assert self.encoder.state.xsize == 200
assert self.encoder.state.ysize == 200
def test_negsize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
fp = BytesIO()
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
)
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")]
)
def test_oversize(self):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
fp = BytesIO()
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")],
)
with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")],
)
def test_encode(self):
encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError):
encoder.encode(None)
bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0
assert ImageFile.ERRORS[errcode] == "bad configuration"
encoder._pushes_fd = True
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)

View File

@ -29,7 +29,7 @@ pytestmark = skip_unless_feature("freetype2")
class TestImageFont:
LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC
LAYOUT_ENGINE = ImageFont.Layout.BASIC
def get_font(self):
return ImageFont.truetype(
@ -88,19 +88,6 @@ class TestImageFont:
ImageFont.truetype(tempfile, FONT_SIZE)
def test_unavailable_layout_engine(self):
have_raqm = ImageFont.core.HAVE_RAQM
ImageFont.core.HAVE_RAQM = False
try:
ttf = ImageFont.truetype(
FONT_PATH, FONT_SIZE, layout_engine=ImageFont.LAYOUT_RAQM
)
finally:
ImageFont.core.HAVE_RAQM = have_raqm
assert ttf.layout_engine == ImageFont.LAYOUT_BASIC
def _render(self, font):
txt = "Hello World!"
ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE)
@ -182,7 +169,7 @@ class TestImageFont:
im = Image.new(mode, (1, 1), 0)
d = ImageDraw.Draw(im)
if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC:
if self.LAYOUT_ENGINE == ImageFont.Layout.BASIC:
length = d.textlength(text, f)
assert length == length_basic
else:
@ -294,7 +281,7 @@ class TestImageFont:
word = "testing"
font = self.get_font()
orientation = Image.ROTATE_90
orientation = Image.Transpose.ROTATE_90
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
# Original font
@ -333,7 +320,7 @@ class TestImageFont:
# Arrange
text = "mask this"
font = self.get_font()
orientation = Image.ROTATE_90
orientation = Image.Transpose.ROTATE_90
transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
# Act
@ -604,7 +591,7 @@ class TestImageFont:
# Arrange
t = self.get_font()
# Act / Assert
if t.layout_engine == ImageFont.LAYOUT_BASIC:
if t.layout_engine == ImageFont.Layout.BASIC:
with pytest.raises(KeyError):
t.getmask("абвг", direction="rtl")
with pytest.raises(KeyError):
@ -753,7 +740,7 @@ class TestImageFont:
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
if self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM:
if self.LAYOUT_ENGINE == ImageFont.Layout.RAQM:
width, height = (129, 44)
else:
width, height = (128, 44)
@ -993,7 +980,7 @@ class TestImageFont:
@skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont):
LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM
LAYOUT_ENGINE = ImageFont.Layout.RAQM
def test_render_mono_size():
@ -1004,7 +991,7 @@ def test_render_mono_size():
ttf = ImageFont.truetype(
"Tests/fonts/DejaVuSans/DejaVuSans.ttf",
18,
layout_engine=ImageFont.LAYOUT_BASIC,
layout_engine=ImageFont.Layout.BASIC,
)
draw.text((10, 10), "r" * 10, "black", ttf)
@ -1022,3 +1009,25 @@ def test_oom(test_file):
font = ImageFont.truetype(BytesIO(f.read()))
with pytest.raises(Image.DecompressionBombError):
font.getmask("Test Text")
def test_raqm_missing_warning(monkeypatch):
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record:
font = ImageFont.truetype(
FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM
)
assert font.layout_engine == ImageFont.Layout.BASIC
assert str(record[-1].message) == (
"Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout."
)
def test_constants_deprecation():
for enum, prefix in {
ImageFont.Layout: "LAYOUT_",
}.items():
for name in enum.__members__:
with pytest.warns(DeprecationWarning):
assert getattr(ImageFont, prefix + name) == enum[name]

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import ImageQt
@ -56,7 +58,5 @@ def test_image():
def test_closed_file():
with pytest.warns(None) as record:
with warnings.catch_warnings():
ImageQt.ImageQt("Tests/images/hopper.gif")
assert not record

View File

@ -51,6 +51,16 @@ def test_show():
assert ImageShow.show(im)
def test_show_without_viewers():
viewers = ImageShow._viewers
ImageShow._viewers = []
im = hopper()
assert not ImageShow.show(im)
ImageShow._viewers = viewers
def test_viewer():
viewer = ImageShow.Viewer()

View File

@ -34,7 +34,7 @@ def test_basic(tmp_path):
imOut = imIn.copy()
verify(imOut) # copy
imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h))
imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(imOut) # transform
filename = str(tmp_path / "temp.im")

View File

@ -1,3 +1,5 @@
import warnings
import pytest
from PIL import Image
@ -237,6 +239,5 @@ def test_no_resource_warning_for_numpy_array():
with Image.open(test_file) as im:
# Act/Assert
with pytest.warns(None) as record:
with warnings.catch_warnings():
array(im)
assert not record

View File

@ -3,7 +3,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXBUILD = python3 -m sphinx.cmd.build
PAPER =
BUILDDIR = _build
@ -41,38 +41,48 @@ help:
clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
python3 -c "import sphinx" > /dev/null 2>&1 || python3 -m pip install sphinx
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
@ -82,6 +92,7 @@ qthelp:
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@ -91,11 +102,13 @@ devhelp:
@echo "# devhelp"
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@ -103,22 +116,26 @@ latex:
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@ -126,28 +143,33 @@ texinfo:
"(use \`make info' here to do that automatically)."
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -66,6 +66,82 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest
``viewer.show_file(path="test.jpg")`` instead.
Constants
~~~~~~~~~
.. deprecated:: 9.2.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.
===================================================== ============================================================
Deprecated Use instead
===================================================== ============================================================
``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.ORDERED`` ``Image.Dither.ORDERED``
``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.BILNEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
``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_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC``
``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION``
``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``
``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT``
``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT``
``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF``
``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC``
``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM``
``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED``
``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5``
``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1``
``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED``
``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE``
``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND``
``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS``
``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE``
``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER``
===================================================== ============================================================
FitsStubImagePlugin
~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.1.0
The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead.
Removed features
----------------

View File

@ -8,4 +8,4 @@ Appendices
image-file-formats
text-anchors
writing-your-own-file-decoder
writing-your-own-image-plugin

View File

@ -26,6 +26,20 @@ Fully supported formats
.. contents::
BLP
^^^
BLP is the Blizzard Mipmap Format, a texture format used in World of
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
images, and all types of ``BLP2`` images.
Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method
can take the following keyword arguments:
**blp_version**
If present and set to "BLP1", images will be saved as BLP1. Otherwise, images
will be saved as BLP2.
BMP
^^^
@ -381,11 +395,12 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
* ``keep``: Only valid for JPEG files, will retain the original image setting.
* ``4:4:4``, ``4:2:2``, ``4:2:0``: Specific sampling values
* ``-1``: equivalent to ``keep``
* ``0``: equivalent to ``4:4:4``
* ``1``: equivalent to ``4:2:2``
* ``2``: equivalent to ``4:2:0``
If absent, the setting will be determined by libjpeg or libjpeg-turbo.
**qtables**
If present, sets the qtables for the encoder. This is listed as an
advanced option for wizards in the JPEG documentation. Use with
@ -696,12 +711,12 @@ parameter must be set to ``True``. The following parameters can also be set:
operation to be used for this frame before rendering the next frame.
Defaults to 0.
* 0 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_NONE`, default) -
* 0 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_NONE`, default) -
No disposal is done on this frame before rendering the next frame.
* 1 (:py:data:`PIL.PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`) -
* 1 (:py:data:`PIL.PngImagePlugin.Disposal.OP_BACKGROUND`) -
This frame's modified region is cleared to fully transparent black before
rendering the next frame.
* 2 (:py:data:`~PIL.PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`) -
* 2 (:py:data:`~PIL.PngImagePlugin.Disposal.OP_PREVIOUS`) -
This frame's modified region is reverted to the previous frame's contents before
rendering the next frame.
@ -710,10 +725,10 @@ parameter must be set to ``True``. The following parameters can also be set:
operation to be used for this frame before rendering the next frame.
Defaults to 0.
* 0 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_SOURCE`) -
* 0 (:py:data:`~PIL.PngImagePlugin.Blend.OP_SOURCE`) -
All color components of this frame, including alpha, overwrite the previous output
image contents.
* 1 (:py:data:`~PIL.PngImagePlugin.APNG_BLEND_OP_OVER`) -
* 1 (:py:data:`~PIL.PngImagePlugin.Blend.OP_OVER`) -
This frame should be alpha composited with the previous output image contents.
.. note::
@ -1041,13 +1056,6 @@ Pillow reads and writes X bitmap files (mode ``1``).
Read-only formats
-----------------
BLP
^^^
BLP is the Blizzard Mipmap Format, a texture format used in World of
Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1``
images, and all types of ``BLP2`` images.
CUR
^^^
@ -1064,6 +1072,13 @@ is commonly used in fax applications. The DCX decoder can read files containing
When the file is opened, only the first image is read. You can use
:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
FITS
^^^^
.. versionadded:: 9.1.0
Pillow identifies and reads FITS files, commonly used for astronomy.
FLI, FLC
^^^^^^^^
@ -1354,16 +1369,6 @@ Pillow provides a stub driver for BUFR files.
To add read or write support to your application, use
:py:func:`PIL.BufrStubImagePlugin.register_handler`.
FITS
^^^^
.. versionadded:: 1.1.5
Pillow provides a stub driver for FITS files.
To add read or write support to your application, use
:py:func:`PIL.FitsStubImagePlugin.register_handler`.
GRIB
^^^^

View File

@ -155,7 +155,7 @@ Processing a subrectangle, and pasting it back
::
region = region.transpose(Image.ROTATE_180)
region = region.transpose(Image.Transpose.ROTATE_180)
im.paste(region, box)
When pasting regions back, the size of the region must match the given region
@ -238,11 +238,11 @@ Transposing an image
::
out = im.transpose(Image.FLIP_LEFT_RIGHT)
out = im.transpose(Image.FLIP_TOP_BOTTOM)
out = im.transpose(Image.ROTATE_90)
out = im.transpose(Image.ROTATE_180)
out = im.transpose(Image.ROTATE_270)
out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
out = im.transpose(Image.Transpose.ROTATE_90)
out = im.transpose(Image.Transpose.ROTATE_180)
out = im.transpose(Image.Transpose.ROTATE_270)
``transpose(ROTATE)`` operations can also be performed identically with
:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is

View File

@ -4,10 +4,9 @@ Writing Your Own Image Plugin
=============================
Pillow uses a plugin model which allows you to add your own
decoders to the library, without any changes to the library
itself. Such plugins usually have names like
:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name
(usually an abbreviation).
decoders and encoders to the library, without any changes to the library
itself. Such plugins usually have names like :file:`XxxImagePlugin.py`,
where ``Xxx`` is a unique format name (usually an abbreviation).
.. warning:: Pillow >= 2.1.0 no longer automatically imports any file
in the Python path with a name ending in
@ -413,23 +412,24 @@ value, or if there is a read error from the file. This function should
free any allocated memory and release any resources from external
libraries.
.. _file-decoders-py:
.. _file-codecs-py:
Writing Your Own File Decoder in Python
=======================================
Writing Your Own File Codec in Python
=====================================
Python file decoders should derive from
:py:class:`PIL.ImageFile.PyDecoder` and should at least override the
decode method. File decoders should be registered using
:py:meth:`PIL.Image.register_decoder`. As in the C implementation of
the file decoders, there are three stages in the lifetime of a
Python-based file decoder:
Python file decoders and encoders should derive from
:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder`
respectively, and should at least override the decode or encode method.
They should be registered using :py:meth:`PIL.Image.register_decoder` and
:py:meth:`PIL.Image.register_encoder`. As in the C implementation of
the file codecs, there are three stages in the lifetime of a
Python-based file codec:
1. Setup: Pillow looks for the decoder in the registry, then
instantiates the class.
2. Decoding: The decoder instance's ``decode`` method is repeatedly
called with a buffer of data to be interpreted.
3. Cleanup: The decoder instance's ``cleanup`` method is called.
2. Transforming: The instance's ``decode`` method is repeatedly called with
a buffer of data to be interpreted, or the ``encode`` method is repeatedly
called with the size of data to be output.
3. Cleanup: The instance's ``cleanup`` method is called.

View File

@ -169,7 +169,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.13**.
above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**.
* **libwebp** provides the WebP format.
@ -215,7 +215,7 @@ Many of Pillow's features require external libraries:
Once you have installed the prerequisites, run::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
python3 -m pip install --upgrade Pillow --no-binary :all:
If the prerequisites are installed in the standard library locations
for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
@ -225,7 +225,7 @@ those locations by editing :file:`setup.py` or
:file:`setup.cfg`, or by adding environment variables on the command
line::
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
If Pillow has been previously built without the required
prerequisites, it may be necessary to manually clear the pip cache or
@ -291,7 +291,7 @@ tools.
The easiest way to install external libraries is via `Homebrew
<https://brew.sh/>`_. After you install Homebrew, run::
brew install libtiff libjpeg webp little-cms2
brew install libjpeg libtiff little-cms2 openjpeg webp
To install libraqm on macOS use Homebrew to install its dependencies::
@ -302,7 +302,7 @@ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
Now install Pillow with::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
python3 -m pip install --upgrade Pillow --no-binary :all:
or from within the uncompressed source directory::
@ -349,7 +349,7 @@ Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
Now install Pillow with::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
python3 -m pip install --upgrade Pillow --no-binary :all:
Building on FreeBSD
@ -455,6 +455,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 8 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 10 Buster | 3.7 | x86 |
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 |
@ -463,6 +465,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Fedora 35 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
@ -496,11 +500,11 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.0.0 |arm |
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |arm |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+--------------+
| | 3.7, 3.8, 3.9, 3.10 | 9.0.0 |x86-64 |
| | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |x86-64 |
| +---------------------------+------------------+--------------+
| | 3.6 | 8.4.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+

View File

@ -254,7 +254,8 @@ This rotates the input image by ``theta`` degrees counter clockwise:
.. automethod:: PIL.Image.Image.transform
.. automethod:: PIL.Image.Image.transpose
This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method.
This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`
method.
.. code-block:: python
@ -263,9 +264,9 @@ This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method.
with Image.open("hopper.jpg") as im:
# Flip the image from left to right
im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT)
im_flipped = im.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT)
# To flip the image from top to bottom,
# use the method "Image.FLIP_TOP_BOTTOM"
# use the method "Image.Transpose.FLIP_TOP_BOTTOM"
.. automethod:: PIL.Image.Image.verify
@ -389,68 +390,57 @@ Transpose methods
Used to specify the :meth:`Image.transpose` method to use.
.. data:: FLIP_LEFT_RIGHT
.. data:: FLIP_TOP_BOTTOM
.. data:: ROTATE_90
.. data:: ROTATE_180
.. data:: ROTATE_270
.. data:: TRANSPOSE
.. data:: TRANSVERSE
.. autoclass:: Transpose
:members:
:undoc-members:
Transform methods
^^^^^^^^^^^^^^^^^
Used to specify the :meth:`Image.transform` method to use.
.. data:: AFFINE
.. py:class:: Transform
Affine transform
.. py:attribute:: AFFINE
.. data:: EXTENT
Affine transform
Cut out a rectangular subregion
.. py:attribute:: EXTENT
.. data:: PERSPECTIVE
Cut out a rectangular subregion
Perspective transform
.. py:attribute:: PERSPECTIVE
.. data:: QUAD
Perspective transform
Map a quadrilateral to a rectangle
.. py:attribute:: QUAD
.. data:: MESH
Map a quadrilateral to a rectangle
Map a number of source quadrilaterals in one operation
.. py:attribute:: MESH
Map a number of source quadrilaterals in one operation
Resampling filters
^^^^^^^^^^^^^^^^^^
See :ref:`concept-filters` for details.
.. data:: NEAREST
:noindex:
.. data:: BOX
:noindex:
.. data:: BILINEAR
:noindex:
.. data:: HAMMING
:noindex:
.. data:: BICUBIC
:noindex:
.. data:: LANCZOS
:noindex:
.. autoclass:: Resampling
:members:
:undoc-members:
Some filters are also available under the following names for backwards compatibility:
Some deprecated filters are also available under the following names:
.. data:: NONE
:noindex:
:value: NEAREST
:value: Resampling.NEAREST
.. data:: LINEAR
:value: BILINEAR
:value: Resampling.BILINEAR
.. data:: CUBIC
:value: BICUBIC
:value: Resampling.BICUBIC
.. data:: ANTIALIAS
:value: LANCZOS
:value: Resampling.LANCZOS
Dither modes
^^^^^^^^^^^^
@ -458,48 +448,56 @@ Dither modes
Used to specify the dithering method to use for the
:meth:`~Image.convert` and :meth:`~Image.quantize` methods.
.. data:: NONE
:noindex:
.. py:class:: Dither
No dither
.. py:attribute:: NONE
.. comment: (not implemented)
.. data:: ORDERED
.. data:: RASTERIZE
No dither
.. data:: FLOYDSTEINBERG
.. py:attribute:: ORDERED
Floyd-Steinberg dither
Not implemented
.. py:attribute:: RASTERIZE
Not implemented
.. py:attribute:: FLOYDSTEINBERG
Floyd-Steinberg dither
Palettes
^^^^^^^^
Used to specify the pallete to use for the :meth:`~Image.convert` method.
.. data:: WEB
.. data:: ADAPTIVE
.. autoclass:: Palette
:members:
:undoc-members:
Quantization methods
^^^^^^^^^^^^^^^^^^^^
Used to specify the quantization method to use for the :meth:`~Image.quantize` method.
.. data:: MEDIANCUT
.. py:class:: Quantize
Median cut. Default method, except for RGBA images. This method does not support
RGBA images.
.. py:attribute:: MEDIANCUT
.. data:: MAXCOVERAGE
Median cut. Default method, except for RGBA images. This method does not support
RGBA images.
Maximum coverage. This method does not support RGBA images.
.. py:attribute:: MAXCOVERAGE
.. data:: FASTOCTREE
Maximum coverage. This method does not support RGBA images.
Fast octree. Default method for RGBA images.
.. py:attribute:: FASTOCTREE
.. data:: LIBIMAGEQUANT
Fast octree. Default method for RGBA images.
libimagequant
.. py:attribute:: LIBIMAGEQUANT
Check support using :py:func:`PIL.features.check_feature`
with ``feature="libimagequant"``.
libimagequant
Check support using :py:func:`PIL.features.check_feature` with
``feature="libimagequant"``.

View File

@ -118,8 +118,8 @@ can be easily displayed in a chromaticity diagram, for example).
another profile (usually overridden at run-time, but provided here
for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010).
One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``.
One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, ``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.SATURATION``.
.. py:attribute:: profile_id
:type: bytes
@ -313,14 +313,14 @@ can be easily displayed in a chromaticity diagram, for example).
the CLUT model.
The dictionary is indexed by intents
(``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``,
``ImageCms.INTENT_PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and
``ImageCms.INTENT_SATURATION``).
(``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``,
``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and
``ImageCms.Intent.SATURATION``).
The values are 3-tuples indexed by directions
(``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``,
``ImageCms.DIRECTION_PROOF``).
(``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``,
``ImageCms.Direction.PROOF``).
The elements of the tuple are booleans. If the value is ``True``,
that intent is supported for that direction.
@ -331,14 +331,14 @@ can be easily displayed in a chromaticity diagram, for example).
Returns a dictionary of all supported intents and directions.
The dictionary is indexed by intents
(``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``,
``ImageCms.INTENT_PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and
``ImageCms.INTENT_SATURATION``).
(``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``,
``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and
``ImageCms.Intent.SATURATION``).
The values are 3-tuples indexed by directions
(``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``,
``ImageCms.DIRECTION_PROOF``).
(``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``,
``ImageCms.Direction.PROOF``).
The elements of the tuple are booleans. If the value is ``True``,
that intent is supported for that direction.
@ -352,11 +352,11 @@ can be easily displayed in a chromaticity diagram, for example).
Note that you can also get this information for all intents and directions
with :py:attr:`.intent_supported`.
:param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``,
``ImageCms.INTENT_PERCEPTUAL``,
``ImageCms.INTENT_RELATIVE_COLORIMETRIC``
and ``ImageCms.INTENT_SATURATION``.
:param direction: One of ``ImageCms.DIRECTION_INPUT``,
``ImageCms.DIRECTION_OUTPUT``
and ``ImageCms.DIRECTION_PROOF``
:param intent: One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``,
``ImageCms.Intent.PERCEPTUAL``,
``ImageCms.Intent.RELATIVE_COLORIMETRIC``
and ``ImageCms.Intent.SATURATION``.
:param direction: One of ``ImageCms.Direction.INPUT``,
``ImageCms.Direction.OUTPUT``
and ``ImageCms.Direction.PROOF``
:return: Boolean if the intent and direction is supported.

View File

@ -40,8 +40,16 @@ Classes
.. autoclass:: PIL.ImageFile.Parser()
:members:
.. autoclass:: PIL.ImageFile.PyCodec()
:members:
.. autoclass:: PIL.ImageFile.PyDecoder()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.PyEncoder()
:members:
:show-inheritance:
.. autoclass:: PIL.ImageFile.ImageFile()
:member-order: bysource

View File

@ -60,12 +60,12 @@ Methods
Constants
---------
.. data:: PIL.ImageFont.LAYOUT_BASIC
.. data:: PIL.ImageFont.Layout.BASIC
Use basic text layout for TrueType font.
Advanced features such as text direction are not supported.
.. data:: PIL.ImageFont.LAYOUT_RAQM
.. data:: PIL.ImageFont.Layout.RAQM
Use Raqm text layout for TrueType font.
Advanced features are supported.

View File

@ -23,6 +23,9 @@ All default viewers convert the image to be shown to PNG format.
.. autoclass:: PIL.ImageShow.EogViewer
.. autoclass:: PIL.ImageShow.XVViewer
To provide maximum functionality on Unix-based systems, temporary files created
from images will not be automatically removed by Pillow.
.. autofunction:: PIL.ImageShow.register
.. autoclass:: PIL.ImageShow.Viewer
:member-order: bysource

View File

@ -53,7 +53,7 @@ Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym``
virtualenv -p python3.8-dbg ~/vpy38-dbg
source ~/vpy38-dbg/bin/activate
cd ~/Pillow && pip install -r requirements.txt && make install
cd ~/Pillow && make install
Test Case
---------

View File

@ -57,7 +57,7 @@ Support for the following features can be checked:
* ``transp_webp``: Support for transparency in WebP images.
* ``webp_mux``: (compile time) Support for EXIF data in WebP images.
* ``webp_anim``: (compile time) Support for animated WebP images.
* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.

View File

@ -41,10 +41,10 @@ Plugin reference
:undoc-members:
:show-inheritance:
:mod:`~PIL.FitsStubImagePlugin` Module
:mod:`~PIL.FitsImagePlugin` Module
--------------------------------------
.. automodule:: PIL.FitsStubImagePlugin
.. automodule:: PIL.FitsImagePlugin
:members:
:undoc-members:
:show-inheritance:
@ -230,8 +230,7 @@ Plugin reference
.. automodule:: PIL.PngImagePlugin
:members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk,
MAX_TEXT_CHUNK, MAX_TEXT_MEMORY, APNG_BLEND_OP_SOURCE, APNG_BLEND_OP_OVER,
APNG_DISPOSE_OP_NONE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_PREVIOUS
Blend, Disposal, MAX_TEXT_CHUNK, MAX_TEXT_MEMORY
:undoc-members:
:show-inheritance:
:member-order: groupwise

View File

@ -111,16 +111,14 @@ downscaling with libjpeg, which uses supersampling internally, not convolutions.
Image transposition
-------------------
A new method :py:data:`PIL.Image.TRANSPOSE` has been added for the
A new method ``TRANSPOSE`` has been added for the
:py:meth:`~PIL.Image.Image.transpose` operation in addition to
:py:data:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:data:`~PIL.Image.FLIP_TOP_BOTTOM`,
:py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_180`,
:py:data:`~PIL.Image.ROTATE_270`. :py:data:`~PIL.Image.TRANSPOSE` is an algebra
transpose, with an image reflected across its main diagonal.
``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``,
``ROTATE_270``. ``TRANSPOSE`` is an algebra transpose, with an image reflected
across its main diagonal.
The speed of :py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_270`
and :py:data:`~PIL.Image.TRANSPOSE` has been significantly improved for large
images which don't fit in the processor cache.
The speed of ``ROTATE_90``, ``ROTATE_270`` and ``TRANSPOSE`` has been significantly
improved for large images which don't fit in the processor cache.
Gaussian blur and unsharp mask
------------------------------

185
docs/releasenotes/9.1.0.rst Normal file
View File

@ -0,0 +1,185 @@
9.1.0
-----
API Changes
===========
Raise an error when performing a negative crop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now
it will raise a ``ValueError``, to help reduce confusion if a user has unintentionally
provided the wrong arguments.
Added specific error if path coordinate type is incorrect
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Rather than returning a ``SystemError``, passing the incorrect types of coordinates into
a path will now raise a more specific ``ValueError``, with the message "incorrect
coordinate type".
Replace requirements.txt with extras
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Rather than installing all dependencies for docs and tests via ``requirements.txt``,
``extras_require`` is used instead. This installs only those needed and at the same
time as installing Pillow.
For example:
.. code-block:: bash
# Install with dependencies for tests:
python3 -m pip install .[tests]
# Or for building docs:
python3 -m pip install .[docs]
# Or for all:
python3 -m pip install .[docs,tests]
On macOS, the last argument may need to be wrapped in quotes, e.g.
``python3 -m pip install ".[tests]"``
Therefore ``requirements.txt`` has been removed along with the ``make install-req``
command for installing its contents.
Deprecations
============
Constants
^^^^^^^^^
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.
===================================================== ============================================================
Deprecated Use instead
===================================================== ============================================================
``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST``
``Image.ORDERED`` ``Image.Dither.ORDERED``
``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.BILNEAR``
``Image.LINEAR`` ``Image.Resampling.BILNEAR``
``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_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC``
``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION``
``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``
``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT``
``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT``
``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF``
``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC``
``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM``
``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED``
``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT``
``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3``
``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5``
``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1``
``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED``
``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE``
``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND``
``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS``
``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE``
``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER``
===================================================== ============================================================
ImageShow.Viewer.show_file file argument
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been
deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by
``path``.
In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest
``viewer.show_file(path="test.jpg")`` instead.
FitsStubImagePlugin
^^^^^^^^^^^^^^^^^^^
.. deprecated:: 9.1.0
The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead.
API Additions
=============
Added get_photoshop_blocks() to parse Photoshop TIFF tag
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:meth:`~PIL.TiffImagePlugin.TiffImageFile.get_photoshop_blocks` has been added, to
allow users to determine what Photoshop "Image Resource Blocks" are contained within an
image. The keys of the returned dictionary are the image resource IDs.
At present, the information within each block is merely returned as a dictionary with a
"data" entry. This will allow more useful information to be added in the future without
breaking backwards compatibility.
Added rawmode argument to Image.getpalette()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the palette.
A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None``
can be used to return data in the current mode of the palette.
Added PyEncoder
^^^^^^^^^^^^^^^
:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be
written in Python. See :ref:`Writing Your Own File Codec in Python<file-codecs-py>` for
more information.
Other Changes
=============
ImageShow temporary files on Unix
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`,
a temporary file is created from the image. On Unix, Pillow will no longer delete these
files, and instead leave it to the operating system to do so.
Image._repr_pretty_
^^^^^^^^^^^^^^^^^^^
``im._repr_pretty_`` has been added to provide a representation of an image without the
identity of the object. This allows Jupyter to describe an image and have that
description stay the same on subsequent executions of the same code.
Added BLP saving
^^^^^^^^^^^^^^^^
Support has been added for saving BLP images. ``blp_version`` can be used to specify
whether the image should be saved as BLP1 or BLP2, e.g.
``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
9.1.0
9.0.1
9.0.0
8.4.0

View File

@ -1,18 +0,0 @@
# Development, documentation & testing requirements.
black
check-manifest
coverage
defusedxml
markdown2
olefile
packaging
pyroma
pytest
pytest-cov
pytest-timeout
sphinx>=2.4
sphinx-copybutton
sphinx-issues>=3.0.1
sphinx-removed-in
sphinx-rtd-theme>=1.0
sphinxext-opengraph

View File

@ -97,9 +97,9 @@ def testimage():
10456
>>> len(im.tobytes())
49152
>>> _info(im.transform((512, 512), Image.AFFINE, (1,0,0,0,1,0)))
>>> _info(im.transform((512, 512), Image.Transform.AFFINE, (1,0,0,0,1,0)))
(None, 'RGB', (512, 512))
>>> _info(im.transform((512, 512), Image.EXTENT, (32,32,96,96)))
>>> _info(im.transform((512, 512), Image.Transform.EXTENT, (32,32,96,96)))
(None, 'RGB', (512, 512))
The ImageDraw module lets you draw stuff in raster images:

View File

@ -35,6 +35,27 @@ project_urls =
[options]
python_requires = >=3.7
[options.extras_require]
docs =
olefile
sphinx>=2.4
sphinx-copybutton
sphinx-issues>=3.0.1
sphinx-removed-in
sphinx-rtd-theme>=1.0
sphinxext-opengraph
tests =
check-manifest
coverage
defusedxml
markdown2
olefile
packaging
pyroma
pytest
pytest-cov
pytest-timeout
[flake8]
extend-ignore = E203
max-line-length = 88

View File

@ -29,20 +29,56 @@ BLP files come in many different flavours:
- DXT5 compression is used if alpha_encoding == 7.
"""
import os
import struct
import warnings
from enum import IntEnum
from io import BytesIO
from . import Image, ImageFile
BLP_FORMAT_JPEG = 0
BLP_ENCODING_UNCOMPRESSED = 1
BLP_ENCODING_DXT = 2
BLP_ENCODING_UNCOMPRESSED_RAW_BGRA = 3
class Format(IntEnum):
JPEG = 0
BLP_ALPHA_ENCODING_DXT1 = 0
BLP_ALPHA_ENCODING_DXT3 = 1
BLP_ALPHA_ENCODING_DXT5 = 7
class Encoding(IntEnum):
UNCOMPRESSED = 1
DXT = 2
UNCOMPRESSED_RAW_BGRA = 3
class AlphaEncoding(IntEnum):
DXT1 = 0
DXT3 = 1
DXT5 = 7
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {
Format: "BLP_FORMAT_",
Encoding: "BLP_ENCODING_",
AlphaEncoding: "BLP_ALPHA_ENCODING_",
}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
def unpack_565(i):
@ -231,6 +267,10 @@ class BLPFormatError(NotImplementedError):
pass
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BlpImageFile(ImageFile.ImageFile):
"""
Blizzard Mipmap Format
@ -241,51 +281,52 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self):
self.magic = self.fp.read(4)
self._read_blp_header()
if self.magic == b"BLP1":
decoder = "BLP1"
self.mode = "RGB"
elif self.magic == b"BLP2":
decoder = "BLP2"
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.fp.seek(5, os.SEEK_CUR)
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
self.fp.seek(2, os.SEEK_CUR)
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}")
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
def _read_blp_header(self):
(self._blp_compression,) = struct.unpack("<i", self.fp.read(4))
(self._blp_encoding,) = struct.unpack("<b", self.fp.read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self.fp.read(1))
(self._blp_mips,) = struct.unpack("<b", self.fp.read(1))
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic == b"BLP1":
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self.fp.read(4))
(self._blp_subtype,) = struct.unpack("<i", self.fp.read(4))
self._blp_offsets = struct.unpack("<16I", self.fp.read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self.fp.read(16 * 4))
class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
try:
self.fd.seek(0)
self.magic = self.fd.read(4)
self._read_blp_header()
self._load()
except struct.error as e:
raise OSError("Truncated Blp file") from e
raise OSError("Truncated BLP file") from e
return 0, 0
def _read_blp_header(self):
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
self.fd.seek(1, os.SEEK_CUR) # mips
self.size = struct.unpack("<II", self._safe_read(8))
if isinstance(self, BLP1Decoder):
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
self.fd.seek(4, os.SEEK_CUR) # subtype
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length):
return ImageFile._safe_read(self.fd, length)
@ -299,43 +340,31 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a))
return ret
def _read_blp_header(self):
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_mips,) = struct.unpack("<b", self._safe_read(1))
self.size = struct.unpack("<II", self._safe_read(8))
if self.magic == b"BLP1":
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
(self._blp_subtype,) = struct.unpack("<i", self._safe_read(4))
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _read_bgra(self, palette):
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
d = (r, g, b)
if self._blp_alpha_depth:
d += (a,)
data.extend(d)
return data
class BLP1Decoder(_BLPBaseDecoder):
def _load(self):
if self._blp_compression == BLP_FORMAT_JPEG:
if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream()
elif self._blp_compression == 1:
if self._blp_encoding in (4, 5):
data = bytearray()
palette = self._read_palette()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend([r, g, b])
data = self._read_bgra(palette)
self.set_as_raw(bytes(data))
else:
raise BLPFormatError(
@ -347,7 +376,7 @@ class BLP1Decoder(_BLPBaseDecoder):
)
def _decode_jpeg_stream(self):
from PIL.JpegImagePlugin import JpegImageFile
from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
@ -357,33 +386,26 @@ class BLP1Decoder(_BLPBaseDecoder):
data = BytesIO(data)
image = JpegImageFile(data)
Image._decompression_bomb_check(image.size)
self.tile = image.tile # :/
self.fd = image.fp
self.mode = image.mode
image.mode = "RGB"
image.tile = [("jpeg", (0, 0) + self.size, 0, ("BGRX", ""))]
self.set_as_raw(image.tobytes())
class BLP2Decoder(_BLPBaseDecoder):
def _load(self):
palette = self._read_palette()
data = bytearray()
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
# Uncompressed or DirectX compression
if self._blp_encoding == BLP_ENCODING_UNCOMPRESSED:
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
data.extend((r, g, b))
if self._blp_encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette)
elif self._blp_encoding == BLP_ENCODING_DXT:
if self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT1:
elif self._blp_encoding == Encoding.DXT:
data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt1(
@ -391,13 +413,13 @@ class BLP2Decoder(_BLPBaseDecoder):
):
data += d
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT3:
elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)):
data += d
elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT5:
elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)):
@ -417,12 +439,59 @@ class BLP2Decoder(_BLPBaseDecoder):
self.set_as_raw(bytes(data))
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True
def _write_palette(self):
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(256):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a)
return data
def encode(self, bufsize):
palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15))
w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15))
data += palette_data
for y in range(h):
for x in range(w):
data += struct.pack("<B", self.im.getpixel((x, y)))
return len(data), 0, data
def _save(im, fp, filename, save_all=False):
if im.mode != "P":
raise ValueError("Unsupported BLP image mode")
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1":
fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0))
ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)])
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
Image.register_extension(BlpImageFile.format, ".blp")
Image.register_decoder("BLP1", BLP1Decoder)
Image.register_decoder("BLP2", BLP2Decoder)
Image.register_save(BlpImageFile.format, _save)
Image.register_encoder("BLP", BLPEncoder)

View File

@ -59,7 +59,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
if _handler is None or not hasattr(_handler, "save"):
raise OSError("BUFR save handler not installed")
_handler.save(im, fp, filename)

View File

@ -0,0 +1,71 @@
#
# The Python Imaging Library
# $Id$
#
# FITS file handling
#
# Copyright (c) 1998-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import math
from . import Image, ImageFile
def _accept(prefix):
return prefix[:6] == b"SIMPLE"
class FitsImageFile(ImageFile.ImageFile):
format = "FITS"
format_description = "FITS"
def _open(self):
headers = {}
while True:
header = self.fp.read(80)
if not header:
raise OSError("Truncated FITS file")
keyword = header[:8].strip()
if keyword == b"END":
break
value = header[8:].strip()
if value.startswith(b"="):
value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"):
raise SyntaxError("Not a FITS file")
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
raise ValueError("No image data")
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
number_of_bits = int(headers[b"BITPIX"])
if number_of_bits == 8:
self.mode = "L"
elif number_of_bits == 16:
self.mode = "I"
# rawmode = "I;16S"
elif number_of_bits == 32:
self.mode = "I"
elif number_of_bits in (-32, -64):
self.mode = "F"
# rawmode = "F" if number_of_bits == -32 else "F;64F"
offset = math.ceil(self.fp.tell() / 2880) * 2880
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
# --------------------------------------------------------------------
# Registry
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])

View File

@ -9,7 +9,9 @@
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
import warnings
from . import FitsImagePlugin, Image, ImageFile
_handler = None
@ -23,57 +25,37 @@ def register_handler(handler):
global _handler
_handler = handler
warnings.warn(
"FitsStubImagePlugin is deprecated and will be removed in Pillow "
"10 (2023-07-01). FITS images can now be read without a handler through "
"FitsImagePlugin instead.",
DeprecationWarning,
)
# --------------------------------------------------------------------
# Image adapter
# Override FitsImagePlugin with this handler
# for backwards compatibility
try:
Image.ID.remove(FITSStubImageFile.format)
except ValueError:
pass
def _accept(prefix):
return prefix[:6] == b"SIMPLE"
Image.register_open(
FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept
)
class FITSStubImageFile(ImageFile.StubImageFile):
format = "FITS"
format_description = "FITS"
format = FitsImagePlugin.FitsImageFile.format
format_description = FitsImagePlugin.FitsImageFile.format_description
def _open(self):
offset = self.fp.tell()
headers = {}
while True:
header = self.fp.read(80)
if not header:
raise OSError("Truncated FITS file")
keyword = header[:8].strip()
if keyword == b"END":
break
value = header[8:].strip()
if value.startswith(b"="):
value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"):
raise SyntaxError("Not a FITS file")
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
raise ValueError("No image data")
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
number_of_bits = int(headers[b"BITPIX"])
if number_of_bits == 8:
self.mode = "L"
elif number_of_bits == 16:
self.mode = "I"
# rawmode = "I;16S"
elif number_of_bits == 32:
self.mode = "I"
elif number_of_bits in (-32, -64):
self.mode = "F"
# rawmode = "F" if number_of_bits == -32 else "F;64F"
im = FitsImagePlugin.FitsImageFile(self.fp)
self._size = im.size
self.mode = im.mode
self.tile = []
self.fp.seek(offset)
@ -86,15 +68,10 @@ class FITSStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
raise OSError("FITS save handler not installed")
_handler.save(im, fp, filename)
raise OSError("FITS save handler not installed")
# --------------------------------------------------------------------
# Registry
Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept)
Image.register_save(FITSStubImageFile.format, _save)
Image.register_extensions(FITSStubImageFile.format, [".fit", ".fits"])

View File

@ -52,13 +52,41 @@ Note: All data is stored in little-Endian (Intel) byte order.
"""
import struct
import warnings
from enum import IntEnum
from io import BytesIO
from . import Image, ImageFile
MAGIC = b"FTEX"
FORMAT_DXT1 = 0
FORMAT_UNCOMPRESSED = 1
class Format(IntEnum):
DXT1 = 0
UNCOMPRESSED = 1
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Format: "FORMAT_"}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
class FtexImageFile(ImageFile.ImageFile):
@ -83,10 +111,10 @@ class FtexImageFile(ImageFile.ImageFile):
data = self.fp.read(mipmap_size)
if format == FORMAT_DXT1:
if format == Format.DXT1:
self.mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, (1))]
elif format == FORMAT_UNCOMPRESSED:
elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
else:
raise ValueError(f"Invalid texture compression format: {repr(format)}")

View File

@ -169,12 +169,12 @@ class GifImageFile(ImageFile.ImageFile):
if "transparency" in self.info:
self.mode = "RGBA"
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.FLOYDSTEINBERG)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
del self.info["transparency"]
else:
self.mode = "RGB"
self.im = self.im.convert("RGB", Image.FLOYDSTEINBERG)
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
if self.dispose:
self.im.paste(self.dispose, self.dispose_extent)
@ -425,7 +425,7 @@ def _normalize_mode(im, initial_call=False):
palette_size = 256
if im.palette:
palette_size = len(im.palette.getdata()[1]) // 3
im = im.convert("P", palette=Image.ADAPTIVE, colors=palette_size)
im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size)
if im.palette.mode == "RGBA":
for rgba in im.palette.colors.keys():
if rgba[3] == 0:

View File

@ -59,7 +59,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
if _handler is None or not hasattr(_handler, "save"):
raise OSError("GRIB save handler not installed")
_handler.save(im, fp, filename)

View File

@ -59,7 +59,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr("_handler", "save"):
if _handler is None or not hasattr(_handler, "save"):
raise OSError("HDF5 save handler not installed")
_handler.save(im, fp, filename)

View File

@ -69,7 +69,7 @@ def _save(im, fp, filename):
if not tmp:
# TODO: invent a more convenient method for proportional scalings
tmp = im.copy()
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None)
tmp.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
fp.write(struct.pack("<H", bits)) # wBitCount(2)

View File

@ -37,6 +37,7 @@ import sys
import tempfile
import warnings
from collections.abc import Callable, MutableMapping
from enum import IntEnum
from pathlib import Path
try:
@ -53,15 +54,57 @@ from ._util import deferred_error, isPath
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
if name in categories:
warnings.warn(
"Image categories are deprecated and will be removed in Pillow 10 "
"(2023-07-01). Use is_animated instead.",
"Image categories are " + deprecated + "Use is_animated instead.",
DeprecationWarning,
stacklevel=2,
)
return categories[name]
elif name in ("NEAREST", "NONE"):
warnings.warn(
name
+ " is "
+ deprecated
+ "Use Resampling.NEAREST or Dither.NONE instead.",
DeprecationWarning,
stacklevel=2,
)
return 0
old_resampling = {
"LINEAR": "BILINEAR",
"CUBIC": "BICUBIC",
"ANTIALIAS": "LANCZOS",
}
if name in old_resampling:
warnings.warn(
name
+ " is "
+ deprecated
+ "Use Resampling."
+ old_resampling[name]
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return Resampling[old_resampling[name]]
for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
if name in enum.__members__:
warnings.warn(
name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@ -139,46 +182,64 @@ def isImageType(t):
# Constants
# transpose
FLIP_LEFT_RIGHT = 0
FLIP_TOP_BOTTOM = 1
ROTATE_90 = 2
ROTATE_180 = 3
ROTATE_270 = 4
TRANSPOSE = 5
TRANSVERSE = 6
class Transpose(IntEnum):
FLIP_LEFT_RIGHT = 0
FLIP_TOP_BOTTOM = 1
ROTATE_90 = 2
ROTATE_180 = 3
ROTATE_270 = 4
TRANSPOSE = 5
TRANSVERSE = 6
# transforms (also defined in Imaging.h)
AFFINE = 0
EXTENT = 1
PERSPECTIVE = 2
QUAD = 3
MESH = 4
class Transform(IntEnum):
AFFINE = 0
EXTENT = 1
PERSPECTIVE = 2
QUAD = 3
MESH = 4
# resampling filters (also defined in Imaging.h)
NEAREST = NONE = 0
BOX = 4
BILINEAR = LINEAR = 2
HAMMING = 5
BICUBIC = CUBIC = 3
LANCZOS = ANTIALIAS = 1
class Resampling(IntEnum):
NEAREST = 0
BOX = 4
BILINEAR = 2
HAMMING = 5
BICUBIC = 3
LANCZOS = 1
_filters_support = {BOX: 0.5, BILINEAR: 1.0, HAMMING: 1.0, BICUBIC: 2.0, LANCZOS: 3.0}
_filters_support = {
Resampling.BOX: 0.5,
Resampling.BILINEAR: 1.0,
Resampling.HAMMING: 1.0,
Resampling.BICUBIC: 2.0,
Resampling.LANCZOS: 3.0,
}
# dithers
NEAREST = NONE = 0
ORDERED = 1 # Not yet implemented
RASTERIZE = 2 # Not yet implemented
FLOYDSTEINBERG = 3 # default
class Dither(IntEnum):
NONE = 0
ORDERED = 1 # Not yet implemented
RASTERIZE = 2 # Not yet implemented
FLOYDSTEINBERG = 3 # default
# palettes/quantizers
WEB = 0
ADAPTIVE = 1
class Palette(IntEnum):
WEB = 0
ADAPTIVE = 1
class Quantize(IntEnum):
MEDIANCUT = 0
MAXCOVERAGE = 1
FASTOCTREE = 2
LIBIMAGEQUANT = 3
MEDIANCUT = 0
MAXCOVERAGE = 1
FASTOCTREE = 2
LIBIMAGEQUANT = 3
if hasattr(core, "DEFAULT_STRATEGY"):
DEFAULT_STRATEGY = core.DEFAULT_STRATEGY
@ -203,48 +264,16 @@ ENCODERS = {}
# --------------------------------------------------------------------
# Modes
if sys.byteorder == "little":
_ENDIAN = "<"
else:
_ENDIAN = ">"
_MODE_CONV = {
# official modes
"1": ("|b1", None), # Bits need to be extended to bytes
"L": ("|u1", None),
"LA": ("|u1", 2),
"I": (_ENDIAN + "i4", None),
"F": (_ENDIAN + "f4", None),
"P": ("|u1", None),
"RGB": ("|u1", 3),
"RGBX": ("|u1", 4),
"RGBA": ("|u1", 4),
"CMYK": ("|u1", 4),
"YCbCr": ("|u1", 3),
"LAB": ("|u1", 3), # UNDONE - unsigned |u1i1i1
"HSV": ("|u1", 3),
# I;16 == I;16L, and I;32 == I;32L
"I;16": ("<u2", None),
"I;16B": (">u2", None),
"I;16L": ("<u2", None),
"I;16S": ("<i2", None),
"I;16BS": (">i2", None),
"I;16LS": ("<i2", None),
"I;32": ("<u4", None),
"I;32B": (">u4", None),
"I;32L": ("<u4", None),
"I;32S": ("<i4", None),
"I;32BS": (">i4", None),
"I;32LS": ("<i4", None),
}
_ENDIAN = "<" if sys.byteorder == "little" else ">"
def _conv_type_shape(im):
typ, extra = _MODE_CONV[im.mode]
if extra is None:
return (im.size[1], im.size[0]), typ
else:
return (im.size[1], im.size[0], extra), typ
m = ImageMode.getmode(im.mode)
shape = (im.height, im.width)
extra = len(m.bands)
if extra != 1:
shape += (extra,)
return shape, m.typestr
MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"]
@ -821,13 +850,7 @@ class Image:
if self.im and self.palette and self.palette.dirty:
# realize palette
mode, arr = self.palette.getdata()
if mode == "RGBA":
mode = "RGB"
self.info["transparency"] = arr[3::4]
arr = bytes(
value for (index, value) in enumerate(arr) if index % 4 != 3
)
palette_length = self.im.putpalette(mode, arr)
self.im.putpalette(mode, arr)
self.palette.dirty = 0
self.palette.rawmode = None
if "transparency" in self.info and mode in ("LA", "PA"):
@ -837,8 +860,9 @@ class Image:
self.im.putpalettealphas(self.info["transparency"])
self.palette.mode = "RGBA"
else:
self.palette.mode = "RGB"
self.palette.palette = self.im.getpalette()[: palette_length * 3]
palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB"
self.palette.mode = palette_mode
self.palette.palette = self.im.getpalette(palette_mode, palette_mode)
if self.im:
if cffi and USE_CFFI_ACCESS:
@ -862,7 +886,9 @@ class Image:
"""
pass
def convert(self, mode=None, matrix=None, dither=None, palette=WEB, colors=256):
def convert(
self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256
):
"""
Returns a converted copy of this image. For the "P" mode, this
method translates pixels through the palette. If mode is
@ -881,7 +907,7 @@ class Image:
The default method of converting a greyscale ("L") or "RGB"
image into a bilevel (mode "1") image uses Floyd-Steinberg
dither to approximate the original image luminosity levels. If
dither is :data:`NONE`, all values larger than 127 are set to 255 (white),
dither is ``None``, all values larger than 127 are set to 255 (white),
all other values to 0 (black). To use other thresholds, use the
:py:meth:`~PIL.Image.Image.point` method.
@ -894,12 +920,13 @@ class Image:
should be 4- or 12-tuple containing floating point values.
:param dither: Dithering method, used when converting from
mode "RGB" to "P" or from "RGB" or "L" to "1".
Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default).
Note that this is not used when ``matrix`` is supplied.
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
(default). Note that this is not used when ``matrix`` is supplied.
:param palette: Palette to use when converting from mode "RGB"
to "P". Available palettes are :data:`WEB` or :data:`ADAPTIVE`.
:param colors: Number of colors to use for the :data:`ADAPTIVE` palette.
Defaults to 256.
to "P". Available palettes are :data:`Palette.WEB` or
:data:`Palette.ADAPTIVE`.
:param colors: Number of colors to use for the :data:`Palette.ADAPTIVE`
palette. Defaults to 256.
:rtype: :py:class:`~PIL.Image.Image`
:returns: An :py:class:`~PIL.Image.Image` object.
"""
@ -1006,7 +1033,7 @@ class Image:
else:
raise ValueError("Transparency for P mode should be bytes or int")
if mode == "P" and palette == ADAPTIVE:
if mode == "P" and palette == Palette.ADAPTIVE:
im = self.im.quantize(colors)
new = self._new(im)
from . import ImagePalette
@ -1028,7 +1055,7 @@ class Image:
# colorspace conversion
if dither is None:
dither = FLOYDSTEINBERG
dither = Dither.FLOYDSTEINBERG
try:
im = self.im.convert(mode, dither)
@ -1041,7 +1068,7 @@ class Image:
raise ValueError("illegal conversion") from e
new_im = self._new(im)
if mode == "P" and palette != ADAPTIVE:
if mode == "P" and palette != Palette.ADAPTIVE:
from . import ImagePalette
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
@ -1064,31 +1091,38 @@ class Image:
new_im.info["transparency"] = trns
return new_im
def quantize(self, colors=256, method=None, kmeans=0, palette=None, dither=1):
def quantize(
self,
colors=256,
method=None,
kmeans=0,
palette=None,
dither=Dither.FLOYDSTEINBERG,
):
"""
Convert the image to 'P' mode with the specified number
of colors.
:param colors: The desired number of colors, <= 256
:param method: :data:`MEDIANCUT` (median cut),
:data:`MAXCOVERAGE` (maximum coverage),
:data:`FASTOCTREE` (fast octree),
:data:`LIBIMAGEQUANT` (libimagequant; check support using
:py:func:`PIL.features.check_feature`
with ``feature="libimagequant"``).
:param method: :data:`Quantize.MEDIANCUT` (median cut),
:data:`Quantize.MAXCOVERAGE` (maximum coverage),
:data:`Quantize.FASTOCTREE` (fast octree),
:data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support
using :py:func:`PIL.features.check_feature` with
``feature="libimagequant"``).
By default, :data:`MEDIANCUT` will be used.
By default, :data:`Quantize.MEDIANCUT` will be used.
The exception to this is RGBA images. :data:`MEDIANCUT` and
:data:`MAXCOVERAGE` do not support RGBA images, so
:data:`FASTOCTREE` is used by default instead.
The exception to this is RGBA images. :data:`Quantize.MEDIANCUT`
and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so
:data:`Quantize.FASTOCTREE` is used by default instead.
:param kmeans: Integer
:param palette: Quantize to the palette of given
:py:class:`PIL.Image.Image`.
:param dither: Dithering method, used when converting from
mode "RGB" to "P" or from "RGB" or "L" to "1".
Available methods are :data:`NONE` or :data:`FLOYDSTEINBERG` (default).
Default: 1 (legacy setting)
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
(default).
:returns: A new image
"""
@ -1097,11 +1131,14 @@ class Image:
if method is None:
# defaults:
method = MEDIANCUT
method = Quantize.MEDIANCUT
if self.mode == "RGBA":
method = FASTOCTREE
method = Quantize.FASTOCTREE
if self.mode == "RGBA" and method not in (FASTOCTREE, LIBIMAGEQUANT):
if self.mode == "RGBA" and method not in (
Quantize.FASTOCTREE,
Quantize.LIBIMAGEQUANT,
):
# Caller specified an invalid mode.
raise ValueError(
"Fast Octree (method == 2) and libimagequant (method == 3) "
@ -1404,19 +1441,27 @@ class Image:
self.load()
return self.im.ptr
def getpalette(self):
def getpalette(self, rawmode="RGB"):
"""
Returns the image palette as a list.
:param rawmode: The mode in which to return the palette. ``None`` will
return the palette in its current mode.
.. versionadded:: 9.1.0
:returns: A list of color values [r, g, b, ...], or None if the
image has no palette.
"""
self.load()
try:
return list(self.im.getpalette())
mode = self.im.getpalettemode()
except ValueError:
return None # no palette
if rawmode is None:
rawmode = mode
return list(self.im.getpalette(mode, rawmode))
def getpixel(self, xy):
"""
@ -1761,8 +1806,8 @@ class Image:
Alternatively, an 8-bit string may be used instead of an integer sequence.
:param data: A palette sequence (either a list or a string).
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a
mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L").
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode
that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L").
"""
from . import ImagePalette
@ -1911,15 +1956,18 @@ class Image:
:param size: The requested size in pixels, as a 2-tuple:
(width, height).
:param resample: An optional resampling filter. This can be
one of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`,
:py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`,
:py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`.
one of :py:data:`PIL.Image.Resampling.NEAREST`,
:py:data:`PIL.Image.Resampling.BOX`,
:py:data:`PIL.Image.Resampling.BILINEAR`,
:py:data:`PIL.Image.Resampling.HAMMING`,
:py:data:`PIL.Image.Resampling.BICUBIC` or
:py:data:`PIL.Image.Resampling.LANCZOS`.
If the image has mode "1" or "P", it is always set to
:py:data:`PIL.Image.NEAREST`.
:py:data:`PIL.Image.Resampling.NEAREST`.
If the image mode specifies a number of bits, such as "I;16", then the
default filter is :py:data:`PIL.Image.NEAREST`.
Otherwise, the default filter is :py:data:`PIL.Image.BICUBIC`.
See: :ref:`concept-filters`.
default filter is :py:data:`PIL.Image.Resampling.NEAREST`.
Otherwise, the default filter is
:py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`.
:param box: An optional 4-tuple of floats providing
the source image region to be scaled.
The values must be within (0, 0, width, height) rectangle.
@ -1941,19 +1989,26 @@ class Image:
if resample is None:
type_special = ";" in self.mode
resample = NEAREST if type_special else BICUBIC
elif resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING):
resample = Resampling.NEAREST if type_special else Resampling.BICUBIC
elif resample not in (
Resampling.NEAREST,
Resampling.BILINEAR,
Resampling.BICUBIC,
Resampling.LANCZOS,
Resampling.BOX,
Resampling.HAMMING,
):
message = f"Unknown resampling filter ({resample})."
filters = [
f"{filter[1]} ({filter[0]})"
for filter in (
(NEAREST, "Image.NEAREST"),
(LANCZOS, "Image.LANCZOS"),
(BILINEAR, "Image.BILINEAR"),
(BICUBIC, "Image.BICUBIC"),
(BOX, "Image.BOX"),
(HAMMING, "Image.HAMMING"),
(Resampling.NEAREST, "Image.Resampling.NEAREST"),
(Resampling.LANCZOS, "Image.Resampling.LANCZOS"),
(Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
(Resampling.BOX, "Image.Resampling.BOX"),
(Resampling.HAMMING, "Image.Resampling.HAMMING"),
)
]
raise ValueError(
@ -1974,16 +2029,16 @@ class Image:
return self.copy()
if self.mode in ("1", "P"):
resample = NEAREST
resample = Resampling.NEAREST
if self.mode in ["LA", "RGBA"] and resample != NEAREST:
if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST:
im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
im = im.resize(size, resample, box)
return im.convert(self.mode)
self.load()
if reducing_gap is not None and resample != NEAREST:
if reducing_gap is not None and resample != Resampling.NEAREST:
factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1
factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1
if factor_x > 1 or factor_y > 1:
@ -2038,7 +2093,7 @@ class Image:
def rotate(
self,
angle,
resample=NEAREST,
resample=Resampling.NEAREST,
expand=0,
center=None,
translate=None,
@ -2051,12 +2106,12 @@ class Image:
:param angle: In degrees counter clockwise.
:param resample: An optional resampling filter. This can be
one of :py:data:`PIL.Image.NEAREST` (use nearest neighbour),
one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
:py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2
environment), or :py:data:`PIL.Image.BICUBIC`
environment), or :py:data:`PIL.Image.Resampling.BICUBIC`
(cubic spline interpolation in a 4x4 environment).
If omitted, or if the image has mode "1" or "P", it is
set to :py:data:`PIL.Image.NEAREST`. See :ref:`concept-filters`.
set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`.
:param expand: Optional expansion flag. If true, expands the output
image to make it large enough to hold the entire rotated image.
If false or omitted, make the output image the same size as the
@ -2077,9 +2132,11 @@ class Image:
if angle == 0:
return self.copy()
if angle == 180:
return self.transpose(ROTATE_180)
return self.transpose(Transpose.ROTATE_180)
if angle in (90, 270) and (expand or self.width == self.height):
return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270)
return self.transpose(
Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270
)
# Calculate the affine matrix. Note that this is the reverse
# transformation (from destination image to source) because we
@ -2148,7 +2205,9 @@ class Image:
matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix)
w, h = nw, nh
return self.transform((w, h), AFFINE, matrix, resample, fillcolor=fillcolor)
return self.transform(
(w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor
)
def save(self, fp, format=None, **params):
"""
@ -2334,7 +2393,7 @@ class Image:
"""
return 0
def thumbnail(self, size, resample=BICUBIC, reducing_gap=2.0):
def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0):
"""
Make this image into a thumbnail. This method modifies the
image to contain a thumbnail version of itself, no larger than
@ -2350,11 +2409,14 @@ class Image:
:param size: Requested size.
:param resample: Optional resampling filter. This can be one
of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`,
:py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`,
:py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`.
If omitted, it defaults to :py:data:`PIL.Image.BICUBIC`.
(was :py:data:`PIL.Image.NEAREST` prior to version 2.5.0).
of :py:data:`PIL.Image.Resampling.NEAREST`,
:py:data:`PIL.Image.Resampling.BOX`,
:py:data:`PIL.Image.Resampling.BILINEAR`,
:py:data:`PIL.Image.Resampling.HAMMING`,
:py:data:`PIL.Image.Resampling.BICUBIC` or
:py:data:`PIL.Image.Resampling.LANCZOS`.
If omitted, it defaults to :py:data:`PIL.Image.Resampling.BICUBIC`.
(was :py:data:`PIL.Image.Resampling.NEAREST` prior to version 2.5.0).
See: :ref:`concept-filters`.
:param reducing_gap: Apply optimization by resizing the image
in two steps. First, reducing the image by integer times
@ -2409,7 +2471,13 @@ class Image:
# FIXME: the different transform methods need further explanation
# instead of bloating the method docs, add a separate chapter.
def transform(
self, size, method, data=None, resample=NEAREST, fill=1, fillcolor=None
self,
size,
method,
data=None,
resample=Resampling.NEAREST,
fill=1,
fillcolor=None,
):
"""
Transforms this image. This method creates a new image with the
@ -2418,11 +2486,11 @@ class Image:
:param size: The output size.
:param method: The transformation method. This is one of
:py:data:`PIL.Image.EXTENT` (cut out a rectangular subregion),
:py:data:`PIL.Image.AFFINE` (affine transform),
:py:data:`PIL.Image.PERSPECTIVE` (perspective transform),
:py:data:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or
:py:data:`PIL.Image.MESH` (map a number of source quadrilaterals
:py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion),
:py:data:`PIL.Image.Transform.AFFINE` (affine transform),
:py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform),
:py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or
:py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals
in one operation).
It may also be an :py:class:`~PIL.Image.ImageTransformHandler`
@ -2437,16 +2505,16 @@ class Image:
class Example:
def getdata(self):
method = Image.EXTENT
method = Image.Transform.EXTENT
data = (0, 0, 100, 100)
return method, data
:param data: Extra data to the transformation method.
:param resample: Optional resampling filter. It can be one of
:py:data:`PIL.Image.NEAREST` (use nearest neighbour),
:py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2
:py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour),
:py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2
environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline
interpolation in a 4x4 environment). If omitted, or if the image
has mode "1" or "P", it is set to :py:data:`PIL.Image.NEAREST`.
has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`.
See: :ref:`concept-filters`.
:param fill: If ``method`` is an
:py:class:`~PIL.Image.ImageTransformHandler` object, this is one of
@ -2456,7 +2524,7 @@ class Image:
:returns: An :py:class:`~PIL.Image.Image` object.
"""
if self.mode in ("LA", "RGBA") and resample != NEAREST:
if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST:
return (
self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
.transform(size, method, data, resample, fill, fillcolor)
@ -2477,10 +2545,12 @@ class Image:
if self.mode == "P" and self.palette:
im.palette = self.palette.copy()
im.info = self.info.copy()
if method == MESH:
if method == Transform.MESH:
# list of quads
for box, quad in data:
im.__transformer(box, self, QUAD, quad, resample, fillcolor is None)
im.__transformer(
box, self, Transform.QUAD, quad, resample, fillcolor is None
)
else:
im.__transformer(
(0, 0) + size, self, method, data, resample, fillcolor is None
@ -2488,25 +2558,27 @@ class Image:
return im
def __transformer(self, box, image, method, data, resample=NEAREST, fill=1):
def __transformer(
self, box, image, method, data, resample=Resampling.NEAREST, fill=1
):
w = box[2] - box[0]
h = box[3] - box[1]
if method == AFFINE:
if method == Transform.AFFINE:
data = data[0:6]
elif method == EXTENT:
elif method == Transform.EXTENT:
# convert extent to an affine transform
x0, y0, x1, y1 = data
xs = (x1 - x0) / w
ys = (y1 - y0) / h
method = AFFINE
method = Transform.AFFINE
data = (xs, 0, x0, 0, ys, y0)
elif method == PERSPECTIVE:
elif method == Transform.PERSPECTIVE:
data = data[0:8]
elif method == QUAD:
elif method == Transform.QUAD:
# quadrilateral warp. data specifies the four corners
# given as NW, SW, SE, and NE.
nw = data[0:2]
@ -2530,12 +2602,16 @@ class Image:
else:
raise ValueError("unknown transformation method")
if resample not in (NEAREST, BILINEAR, BICUBIC):
if resample in (BOX, HAMMING, LANCZOS):
if resample not in (
Resampling.NEAREST,
Resampling.BILINEAR,
Resampling.BICUBIC,
):
if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS):
message = {
BOX: "Image.BOX",
HAMMING: "Image.HAMMING",
LANCZOS: "Image.LANCZOS/Image.ANTIALIAS",
Resampling.BOX: "Image.Resampling.BOX",
Resampling.HAMMING: "Image.Resampling.HAMMING",
Resampling.LANCZOS: "Image.Resampling.LANCZOS",
}[resample] + f" ({resample}) cannot be used."
else:
message = f"Unknown resampling filter ({resample})."
@ -2543,9 +2619,9 @@ class Image:
filters = [
f"{filter[1]} ({filter[0]})"
for filter in (
(NEAREST, "Image.NEAREST"),
(BILINEAR, "Image.BILINEAR"),
(BICUBIC, "Image.BICUBIC"),
(Resampling.NEAREST, "Image.Resampling.NEAREST"),
(Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
)
]
raise ValueError(
@ -2557,7 +2633,7 @@ class Image:
self.load()
if image.mode in ("1", "P"):
resample = NEAREST
resample = Resampling.NEAREST
self.im.transform2(box, image.im, method, data, resample, fill)
@ -2565,10 +2641,13 @@ class Image:
"""
Transpose image (flip or rotate in 90 degree steps)
:param method: One of :py:data:`PIL.Image.FLIP_LEFT_RIGHT`,
:py:data:`PIL.Image.FLIP_TOP_BOTTOM`, :py:data:`PIL.Image.ROTATE_90`,
:py:data:`PIL.Image.ROTATE_180`, :py:data:`PIL.Image.ROTATE_270`,
:py:data:`PIL.Image.TRANSPOSE` or :py:data:`PIL.Image.TRANSVERSE`.
:param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`,
:py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`,
:py:data:`PIL.Image.Transpose.ROTATE_90`,
:py:data:`PIL.Image.Transpose.ROTATE_180`,
:py:data:`PIL.Image.Transpose.ROTATE_270`,
:py:data:`PIL.Image.Transpose.TRANSPOSE` or
:py:data:`PIL.Image.Transpose.TRANSVERSE`.
:returns: Returns a flipped or rotated copy of this image.
"""

View File

@ -16,6 +16,8 @@
# below for the original description.
import sys
import warnings
from enum import IntEnum
from PIL import Image
@ -100,14 +102,42 @@ core = _imagingcms
#
# intent/direction values
INTENT_PERCEPTUAL = 0
INTENT_RELATIVE_COLORIMETRIC = 1
INTENT_SATURATION = 2
INTENT_ABSOLUTE_COLORIMETRIC = 3
DIRECTION_INPUT = 0
DIRECTION_OUTPUT = 1
DIRECTION_PROOF = 2
class Intent(IntEnum):
PERCEPTUAL = 0
RELATIVE_COLORIMETRIC = 1
SATURATION = 2
ABSOLUTE_COLORIMETRIC = 3
class Direction(IntEnum):
INPUT = 0
OUTPUT = 1
PROOF = 2
def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
warnings.warn(
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
#
# flags
@ -211,9 +241,9 @@ class ImageCmsTransform(Image.ImagePointHandler):
output,
input_mode,
output_mode,
intent=INTENT_PERCEPTUAL,
intent=Intent.PERCEPTUAL,
proof=None,
proof_intent=INTENT_ABSOLUTE_COLORIMETRIC,
proof_intent=Intent.ABSOLUTE_COLORIMETRIC,
flags=0,
):
if proof is None:
@ -295,7 +325,7 @@ def profileToProfile(
im,
inputProfile,
outputProfile,
renderingIntent=INTENT_PERCEPTUAL,
renderingIntent=Intent.PERCEPTUAL,
outputMode=None,
inPlace=False,
flags=0,
@ -331,10 +361,10 @@ def profileToProfile(
:param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.Intent.SATURATION = 2
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what
they do.
@ -412,7 +442,7 @@ def buildTransform(
outputProfile,
inMode,
outMode,
renderingIntent=INTENT_PERCEPTUAL,
renderingIntent=Intent.PERCEPTUAL,
flags=0,
):
"""
@ -458,10 +488,10 @@ def buildTransform(
:param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.Intent.SATURATION = 2
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what
they do.
@ -494,8 +524,8 @@ def buildProofTransform(
proofProfile,
inMode,
outMode,
renderingIntent=INTENT_PERCEPTUAL,
proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC,
renderingIntent=Intent.PERCEPTUAL,
proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC,
flags=FLAGS["SOFTPROOFING"],
):
"""
@ -550,20 +580,20 @@ def buildProofTransform(
:param renderingIntent: Integer (0-3) specifying the rendering intent you
wish to use for the input->proof (simulated) transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.Intent.SATURATION = 2
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what
they do.
:param proofRenderingIntent: Integer (0-3) specifying the rendering intent
you wish to use for proof->output transform
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.Intent.SATURATION = 2
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what
they do.
@ -922,10 +952,10 @@ def getDefaultIntent(profile):
:returns: Integer 0-3 specifying the default rendering intent for this
profile.
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.Intent.SATURATION = 2
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what
they do.
@ -960,19 +990,19 @@ def isIntentSupported(profile, intent, direction):
:param intent: Integer (0-3) specifying the rendering intent you wish to
use with this profile
ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT)
ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1
ImageCms.INTENT_SATURATION = 2
ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3
ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
ImageCms.Intent.SATURATION = 2
ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
see the pyCMS documentation for details on rendering intents and what
they do.
:param direction: Integer specifying if the profile is to be used for
input, output, or proof
INPUT = 0 (or use ImageCms.DIRECTION_INPUT)
OUTPUT = 1 (or use ImageCms.DIRECTION_OUTPUT)
PROOF = 2 (or use ImageCms.DIRECTION_PROOF)
INPUT = 0 (or use ImageCms.Direction.INPUT)
OUTPUT = 1 (or use ImageCms.Direction.OUTPUT)
PROOF = 2 (or use ImageCms.Direction.PROOF)
:returns: 1 if the intent/direction are supported, -1 if they are not.
:exception PyCMSError:

View File

@ -49,7 +49,11 @@ ERRORS = {
-8: "bad configuration",
-9: "out of memory error",
}
"""Dict of known error codes returned from :meth:`.PyDecoder.decode`."""
"""
Dict of known error codes returned from :meth:`.PyDecoder.decode`,
:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and
:meth:`.PyEncoder.encode_to_file`.
"""
#
@ -577,16 +581,7 @@ class PyCodecState:
return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize)
class PyDecoder:
"""
Python implementation of a format decoder. Override this class and
add the decoding logic in the :meth:`decode` method.
See :ref:`Writing Your Own File Decoder in Python<file-decoders-py>`
"""
_pulls_fd = False
class PyCodec:
def __init__(self, mode, *args):
self.im = None
self.state = PyCodecState()
@ -596,31 +591,16 @@ class PyDecoder:
def init(self, args):
"""
Override to perform decoder specific initialization
Override to perform codec specific initialization
:param args: Array of args items from the tile entry
:returns: None
"""
self.args = args
@property
def pulls_fd(self):
return self._pulls_fd
def decode(self, buffer):
"""
Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded.
:returns: A tuple of ``(bytes consumed, errcode)``.
If finished with decoding return <0 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def cleanup(self):
"""
Override to perform decoder specific cleanup
Override to perform codec specific cleanup
:returns: None
"""
@ -628,16 +608,16 @@ class PyDecoder:
def setfd(self, fd):
"""
Called from ImageFile to set the python file-like object
Called from ImageFile to set the Python file-like object
:param fd: A python file-like object
:param fd: A Python file-like object
:returns: None
"""
self.fd = fd
def setimage(self, im, extents=None):
"""
Called from ImageFile to set the core output image for the decoder
Called from ImageFile to set the core output image for the codec
:param im: A core image object
:param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
@ -670,6 +650,32 @@ class PyDecoder:
):
raise ValueError("Tile cannot extend outside image")
class PyDecoder(PyCodec):
"""
Python implementation of a format decoder. Override this class and
add the decoding logic in the :meth:`decode` method.
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
"""
_pulls_fd = False
@property
def pulls_fd(self):
return self._pulls_fd
def decode(self, buffer):
"""
Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded.
:returns: A tuple of ``(bytes consumed, errcode)``.
If finished with decoding return 0 for the bytes consumed.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def set_as_raw(self, data, rawmode=None):
"""
Convenience method to set the internal image from a stream of raw data
@ -690,3 +696,57 @@ class PyDecoder:
raise ValueError("not enough image data")
if s[1] != 0:
raise ValueError("cannot decode image data")
class PyEncoder(PyCodec):
"""
Python implementation of a format encoder. Override this class and
add the decoding logic in the :meth:`encode` method.
See :ref:`Writing Your Own File Codec in Python<file-codecs-py>`
"""
_pushes_fd = False
@property
def pushes_fd(self):
return self._pushes_fd
def encode(self, bufsize):
"""
Override to perform the encoding process.
:param bufsize: Buffer size.
:returns: A tuple of ``(bytes encoded, errcode, bytes)``.
If finished with encoding return 1 for the error code.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
raise NotImplementedError()
def encode_to_pyfd(self):
"""
:returns: A tuple of ``(bytes consumed, errcode)``.
Err codes are from :data:`.ImageFile.ERRORS`.
"""
if not self.pushes_fd:
return 0, -8 # bad configuration
bytes_consumed, errcode, data = self.encode(0)
if data:
self.fd.write(data)
return bytes_consumed, errcode
def encode_to_file(self, fh, bufsize):
"""
:param fh: File handle.
:param bufsize: Buffer size.
:returns: If finished successfully, return 0.
Otherwise, return an error code. Err codes are from
:data:`.ImageFile.ERRORS`.
"""
errcode = 0
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
return errcode

View File

@ -529,7 +529,7 @@ class Color3DLUT(MultibandFilter):
return image.color_lut_3d(
self.mode or image.mode,
Image.LINEAR,
Image.Resampling.BILINEAR,
self.channels,
self.size[0],
self.size[1],

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