Merge branch 'main' into set_variation_by_name

This commit is contained in:
Andrew Murray 2022-09-17 00:01:05 +10:00
commit 17b56b9463
83 changed files with 2830 additions and 2393 deletions

View File

@ -35,11 +35,9 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install test-image-results
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Remove condition when NumPy supports 3.11 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

View File

@ -11,6 +11,9 @@ on:
- "**.h" - "**.h"
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
Fuzzing: Fuzzing:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -12,11 +12,9 @@ python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install test-image-results
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
# TODO Remove condition when NumPy supports 3.11 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -2,6 +2,9 @@ name: Test Cygwin
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest

View File

@ -24,7 +24,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
docker: [ docker: [
ubuntu-20.04-focal-amd64-valgrind, ubuntu-22.04-jammy-amd64-valgrind,
] ]
dockerTag: [main] dockerTag: [main]

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.6.0 rev: 22.8.0
hooks: hooks:
- id: black - id: black
args: ["--target-version", "py37"] args: ["--target-version", "py37"]
@ -14,18 +14,18 @@ repos:
- id: isort - id: isort
- repo: https://github.com/asottile/yesqa - repo: https://github.com/asottile/yesqa
rev: v1.3.0 rev: v1.4.0
hooks: hooks:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.2.0 rev: v1.3.1
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 4.0.1 rev: 5.0.4
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat] additional_dependencies: [flake8-2020, flake8-implicit-str-concat]

View File

@ -5,6 +5,60 @@ Changelog (Pillow)
9.3.0 (unreleased) 9.3.0 (unreleased)
------------------ ------------------
- Corrected BMP and TGA palette size when saving #6500
[radarhere]
- Do not call load() before draft() in Image.thumbnail #6539
[radarhere]
- Copy palette when converting from P to PA #6497
[radarhere]
- Allow RGB and RGBA values for PA image putpixel #6504
[radarhere]
- Removed support for tkinter in PyPy before Python 3.6 #6551
[nulano]
- Do not use CCITTFaxDecode filter if libtiff is not available #6518
[radarhere]
- Fallback to not using mmap if buffer is not large enough #6510
[radarhere]
- Fixed writing bytes as ASCII tag #6493
[radarhere]
- Open 1 bit EPS in mode 1 #6499
[radarhere]
- Removed support for tkinter before Python 1.5.2 #6549
[radarhere]
- Allow default ImageDraw font to be set #6484
[radarhere, hugovk]
- Save 1 mode PDF using CCITTFaxDecode filter #6470
[radarhere]
- Added support for RGBA PSD images #6481
[radarhere]
- Parse orientation from XMP tag contents #6463
[bigcat88, radarhere]
- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457
[REDxEYE, radarhere]
- Do not clear GIF tile when checking number of frames #6455
[radarhere]
- Support saving multiple MPO frames #6444
[radarhere]
- Do not double quote Pillow version for setuptools >= 60 #6450
[radarhere]
- Added ABGR BMP mask mode #6436 - Added ABGR BMP mask mode #6436
[radarhere] [radarhere]

View File

@ -96,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related
## Binary Distributions ## Binary Distributions
### Windows ### Windows
* [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174. * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
* [ ] Download and extract tarball from `@cgohlke` and copy into `dist/` and copy into `dist/`
### Mac and Linux ### Mac and Linux
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):

BIN
Tests/images/1.eps Normal file

Binary file not shown.

BIN
Tests/images/ati1.dds Normal file

Binary file not shown.

BIN
Tests/images/ati1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

BIN
Tests/images/ati2.dds Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

BIN
Tests/images/mmap_error.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
Tests/images/rgba.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -325,8 +325,9 @@ def test_apng_syntax_errors():
pytest.warns(UserWarning, open) pytest.warns(UserWarning, open)
def test_apng_sequence_errors(): @pytest.mark.parametrize(
test_files = [ "test_file",
(
"sequence_start.png", "sequence_start.png",
"sequence_gap.png", "sequence_gap.png",
"sequence_repeat.png", "sequence_repeat.png",
@ -334,10 +335,11 @@ def test_apng_sequence_errors():
"sequence_reorder.png", "sequence_reorder.png",
"sequence_reorder_chunk.png", "sequence_reorder_chunk.png",
"sequence_fdat_fctl.png", "sequence_fdat_fctl.png",
] ),
for f in test_files: )
def test_apng_sequence_errors(test_file):
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{f}") as im: with Image.open(f"Tests/images/apng/{test_file}") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
im.load() im.load()

View File

@ -39,6 +39,13 @@ def test_invalid_file():
BmpImagePlugin.BmpImageFile(fp) BmpImagePlugin.BmpImageFile(fp)
def test_fallback_if_mmap_errors():
# This image has been truncated,
# so that the buffer is not large enough when using mmap
with Image.open("Tests/images/mmap_error.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp")
def test_save_to_bytes(): def test_save_to_bytes():
output = io.BytesIO() output = io.BytesIO()
im = hopper() im = hopper()
@ -51,6 +58,18 @@ def test_save_to_bytes():
assert reloaded.format == "BMP" assert reloaded.format == "BMP"
def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
im.putpalette(colors)
out = str(tmp_path / "temp.bmp")
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors
def test_save_too_large(tmp_path): def test_save_too_large(tmp_path):
outfile = str(tmp_path / "temp.bmp") outfile = str(tmp_path / "temp.bmp")
with Image.new("RGB", (1, 1)) as im: with Image.new("RGB", (1, 1)) as im:

View File

@ -1,3 +1,5 @@
import pytest
from PIL import ContainerIO, Image from PIL import ContainerIO, Image
from .helper import hopper from .helper import hopper
@ -59,9 +61,9 @@ def test_seek_mode_2():
assert container.tell() == 100 assert container.tell() == 100
def test_read_n0(): @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n0(bytesmode):
# Arrange # Arrange
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -75,9 +77,9 @@ def test_read_n0():
assert data == "7\nThis is line 8\n" assert data == "7\nThis is line 8\n"
def test_read_n(): @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n(bytesmode):
# Arrange # Arrange
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -91,9 +93,9 @@ def test_read_n():
assert data == "7\nT" assert data == "7\nT"
def test_read_eof(): @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_eof(bytesmode):
# Arrange # Arrange
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -107,9 +109,9 @@ def test_read_eof():
assert data == "" assert data == ""
def test_readline(): @pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode):
# Arrange # Arrange
for bytesmode in (True, False):
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120) container = ContainerIO.ContainerIO(fh, 0, 120)
@ -122,9 +124,9 @@ def test_readline():
assert data == "This is line 1\n" assert data == "This is line 1\n"
def test_readlines(): @pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode):
# Arrange # Arrange
for bytesmode in (True, False):
expected = [ expected = [
"This is line 1\n", "This is line 1\n",
"This is line 2\n", "This is line 2\n",

View File

@ -10,6 +10,8 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
@ -62,6 +64,32 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
def test_sanity_ati1():
"""Check ATI1 images can be opened"""
with Image.open(TEST_FILE_ATI1) as im:
im.load()
assert im.format == "DDS"
assert im.mode == "L"
assert im.size == (64, 64)
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
def test_sanity_ati2():
"""Check ATI2 images can be opened"""
with Image.open(TEST_FILE_ATI2) as im:
im.load()
assert im.format == "DDS"
assert im.mode == "RGB"
assert im.size == (256, 256)
assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png"))
@pytest.mark.parametrize( @pytest.mark.parametrize(
("image_path", "expected_path"), ("image_path", "expected_path"),
( (

View File

@ -146,6 +146,11 @@ def test_bytesio_object():
assert_image_similar(img, image1_scale1_compare, 5) assert_image_similar(img, image1_scale1_compare, 5)
def test_1_mode():
with Image.open("Tests/images/1.eps") as im:
assert im.mode == "1"
def test_image_mode_not_supported(tmp_path): def test_image_mode_not_supported(tmp_path):
im = hopper("RGBA") im = hopper("RGBA")
tmpfile = str(tmp_path / "temp.eps") tmpfile = str(tmp_path / "temp.eps")

View File

@ -399,6 +399,11 @@ def test_no_change():
assert im.is_animated assert im.is_animated
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
expected = Image.new("P", (1, 1))
assert not im.is_animated
assert_image_equal(im, expected)
def test_eoferror(): def test_eoferror():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:

View File

@ -78,16 +78,13 @@ def test_eoferror():
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_roundtrip(tmp_path): @pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
def roundtrip(mode): def test_roundtrip(mode, tmp_path):
out = str(tmp_path / "temp.im") out = str(tmp_path / "temp.im")
im = hopper(mode) im = hopper(mode)
im.save(out) im.save(out)
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
for mode in ["RGB", "P", "PA"]:
roundtrip(mode)
def test_save_unsupported_mode(tmp_path): def test_save_unsupported_mode(tmp_path):
out = str(tmp_path / "temp.im") out = str(tmp_path / "temp.im")

View File

@ -135,9 +135,9 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
def test_write_metadata(self, tmp_path): @pytest.mark.parametrize("legacy_api", (False, True))
def test_write_metadata(self, legacy_api, tmp_path):
"""Test metadata writing through libtiff""" """Test metadata writing through libtiff"""
for legacy_api in [False, True]:
f = str(tmp_path / "temp.tiff") f = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper_g4.tif") as img: with Image.open("Tests/images/hopper_g4.tif") as img:
img.save(f, tiffinfo=img.tag) img.save(f, tiffinfo=img.tag)
@ -856,7 +856,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_strip_ycbcr_jpeg_2x2_sampling(self): def test_strip_ycbcr_jpeg_2x2_sampling(self):
infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2)
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -864,7 +864,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_strip_ycbcr_jpeg_1x1_sampling(self): def test_strip_ycbcr_jpeg_1x1_sampling(self):
infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/flower2.jpg") assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01)
def test_tiled_cmyk_jpeg(self): def test_tiled_cmyk_jpeg(self):
infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif"
@ -877,7 +877,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiled_ycbcr_jpeg_1x1_sampling(self): def test_tiled_ycbcr_jpeg_1x1_sampling(self):
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/flower2.jpg") assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01)
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -885,7 +885,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_tiled_ycbcr_jpeg_2x2_sampling(self): def test_tiled_ycbcr_jpeg_2x2_sampling(self):
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im: with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5)
def test_strip_planar_rgb(self): def test_strip_planar_rgb(self):
# gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \
@ -1011,14 +1011,18 @@ class TestFileLibTiff(LibTiffTestCase):
# Assert that there are multiple strips # Assert that there are multiple strips
assert len(im.tag_v2[STRIPOFFSETS]) > 1 assert len(im.tag_v2[STRIPOFFSETS]) > 1
def test_save_single_strip(self, tmp_path): @pytest.mark.parametrize("argument", (True, False))
def test_save_single_strip(self, argument, tmp_path):
im = hopper("RGB").resize((256, 256)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
if not argument:
TiffImagePlugin.STRIP_SIZE = 2**18 TiffImagePlugin.STRIP_SIZE = 2**18
try: try:
arguments = {"compression": "tiff_adobe_deflate"}
im.save(out, compression="tiff_adobe_deflate") if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
with Image.open(out) as im: with Image.open(out) as im:
assert len(im.tag_v2[STRIPOFFSETS]) == 1 assert len(im.tag_v2[STRIPOFFSETS]) == 1

View File

@ -5,15 +5,19 @@ import pytest
from PIL import Image from PIL import Image
from .helper import assert_image_similar, is_pypy, skip_unless_feature from .helper import (
assert_image_equal,
assert_image_similar,
is_pypy,
skip_unless_feature,
)
test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg") pytestmark = skip_unless_feature("jpg")
def frame_roundtrip(im, **options): def roundtrip(im, **options):
# Note that for now, there is no MPO saving functionality
out = BytesIO() out = BytesIO()
im.save(out, "MPO", **options) im.save(out, "MPO", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -23,8 +27,8 @@ def frame_roundtrip(im, **options):
return im return im
def test_sanity(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_sanity(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.load() im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
@ -62,21 +66,20 @@ def test_context_manager():
im.load() im.load()
def test_app(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_app(test_file):
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.applist[0][0] == "APP1" assert im.applist[0][0] == "APP1"
assert im.applist[1][0] == "APP2" assert im.applist[1][0] == "APP2"
assert ( assert (
im.applist[1][1][:16] im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
== b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
) )
assert len(im.applist) == 2 assert len(im.applist) == 2
def test_exif(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_exif(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
info = im._getexif() info = im._getexif()
assert info[272] == "Nintendo 3DS" assert info[272] == "Nintendo 3DS"
@ -133,8 +136,8 @@ def test_reload_exif_after_seek():
assert 296 in exif assert 296 in exif
def test_mp(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_mp(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
@ -158,8 +161,8 @@ def test_mp_no_data():
im.seek(1) im.seek(1)
def test_mp_attribute(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_mp_attribute(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
frame_number = 0 frame_number = 0
@ -177,8 +180,8 @@ def test_mp_attribute():
frame_number += 1 frame_number += 1
def test_seek(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_seek(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
# prior to first image raises an error, both blatant and borderline # prior to first image raises an error, both blatant and borderline
@ -221,8 +224,8 @@ def test_eoferror():
im.seek(n_frames - 1) im.seek(n_frames - 1)
def test_image_grab(): @pytest.mark.parametrize("test_file", test_files)
for test_file in test_files: def test_image_grab(test_file):
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
im0 = im.tobytes() im0 = im.tobytes()
@ -236,14 +239,39 @@ def test_image_grab():
assert im0 != im1 assert im0 != im1
def test_save(): @pytest.mark.parametrize("test_file", test_files)
# Note that only individual frames can be saved at present def test_save(test_file):
for test_file in test_files:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
jpg0 = frame_roundtrip(im) jpg0 = roundtrip(im)
assert_image_similar(im, jpg0, 30) assert_image_similar(im, jpg0, 30)
im.seek(1) im.seek(1)
assert im.tell() == 1 assert im.tell() == 1
jpg1 = frame_roundtrip(im) jpg1 = roundtrip(im)
assert_image_similar(im, jpg1, 30) assert_image_similar(im, jpg1, 30)
def test_save_all():
for test_file in test_files:
with Image.open(test_file) as im:
im_reloaded = roundtrip(im, save_all=True)
im.seek(0)
assert_image_similar(im, im_reloaded, 30)
im.seek(1)
im_reloaded.seek(1)
assert_image_similar(im, im_reloaded, 30)
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
im_reloaded.seek(1)
assert_image_similar(im2, im_reloaded, 1)
# Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True)
assert "mp" not in jpg.info

View File

@ -6,7 +6,7 @@ import time
import pytest import pytest
from PIL import Image, PdfParser from PIL import Image, PdfParser, features
from .helper import hopper, mark_if_feature_version from .helper import hopper, mark_if_feature_version
@ -37,13 +37,14 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
return outfile return outfile
@pytest.mark.valgrind_known_error(reason="Temporary skip")
def test_monochrome(tmp_path): def test_monochrome(tmp_path):
# Arrange # Arrange
mode = "1" mode = "1"
# Act / Assert # Act / Assert
outfile = helper_save_as_pdf(tmp_path, mode) outfile = helper_save_as_pdf(tmp_path, mode)
assert os.path.getsize(outfile) < 15000 assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
def test_greyscale(tmp_path): def test_greyscale(tmp_path):

View File

@ -4,7 +4,7 @@ import pytest
from PIL import Image, PsdImagePlugin from PIL import Image, PsdImagePlugin
from .helper import assert_image_similar, hopper, is_pypy from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy
test_file = "Tests/images/hopper.psd" test_file = "Tests/images/hopper.psd"
@ -107,6 +107,11 @@ def test_open_after_exclusive_load():
im.load() im.load()
def test_rgba():
with Image.open("Tests/images/rgba.psd") as im:
assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
def test_icc_profile(): def test_icc_profile():
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert "icc_profile" in im.info assert "icc_profile" in im.info

View File

@ -18,18 +18,15 @@ _ORIGINS = ("tl", "bl")
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
def test_sanity(tmp_path): @pytest.mark.parametrize("mode", _MODES)
for mode in _MODES: def test_sanity(mode, tmp_path):
def roundtrip(original_im): def roundtrip(original_im):
out = str(tmp_path / "temp.tga") out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle) original_im.save(out, rle=rle)
with Image.open(out) as saved_im: with Image.open(out) as saved_im:
if rle: if rle:
assert ( assert saved_im.info["compression"] == original_im.info["compression"]
saved_im.info["compression"] == original_im.info["compression"]
)
assert saved_im.info["orientation"] == original_im.info["orientation"] assert saved_im.info["orientation"] == original_im.info["orientation"]
if mode == "P": if mode == "P":
assert saved_im.getpalette() == original_im.getpalette() assert saved_im.getpalette() == original_im.getpalette()
@ -123,6 +120,18 @@ def test_save(tmp_path):
assert test_im.size == (100, 100) assert test_im.size == (100, 100)
def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 0, 0]
im.putpalette(colors)
out = str(tmp_path / "temp.tga")
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors
def test_save_wrong_mode(tmp_path): def test_save_wrong_mode(tmp_path):
im = hopper("PA") im = hopper("PA")
out = str(tmp_path / "temp.tga") out = str(tmp_path / "temp.tga")

View File

@ -185,6 +185,22 @@ def test_iptc(tmp_path):
im.save(out) im.save(out)
def test_writing_bytes_to_ascii(tmp_path):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
assert tag.type == TiffTags.ASCII
info[271] = b"test"
out = str(tmp_path / "temp.tiff")
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert reloaded.tag_v2[271] == "test"
def test_undefined_zero(tmp_path): def test_undefined_zero(tmp_path):
# Check that the tag has not been changed since this test was created # Check that the tag has not been changed since this test was created
tag = TiffTags.TAGS_V2[45059] tag = TiffTags.TAGS_V2[45059]

View File

@ -66,10 +66,10 @@ def test_load_set_dpi():
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
def test_save(tmp_path): @pytest.mark.parametrize("ext", (".wmf", ".emf"))
def test_save(ext, tmp_path):
im = hopper() im = hopper()
for ext in [".wmf", ".emf"]:
tmpfile = str(tmp_path / ("temp" + ext)) tmpfile = str(tmp_path / ("temp" + ext))
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(tmpfile) im.save(tmpfile)

View File

@ -22,8 +22,9 @@ from .helper import (
class TestImage: class TestImage:
def test_image_modes_success(self): @pytest.mark.parametrize(
for mode in [ "mode",
(
"1", "1",
"P", "P",
"PA", "PA",
@ -44,19 +45,15 @@ class TestImage:
"YCbCr", "YCbCr",
"LAB", "LAB",
"HSV", "HSV",
]: ),
)
def test_image_modes_success(self, mode):
Image.new(mode, (1, 1)) Image.new(mode, (1, 1))
def test_image_modes_fail(self): @pytest.mark.parametrize(
for mode in [ "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32")
"", )
"bad", def test_image_modes_fail(self, mode):
"very very long",
"BGR;15",
"BGR;16",
"BGR;24",
"BGR;32",
]:
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
Image.new(mode, (1, 1)) Image.new(mode, (1, 1))
assert str(e.value) == "unrecognized image mode" assert str(e.value) == "unrecognized image mode"
@ -539,11 +536,10 @@ class TestImage:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.linear_gradient(wrong_mode) Image.linear_gradient(wrong_mode)
def test_linear_gradient(self): @pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
def test_linear_gradient(self, mode):
# Arrange # Arrange
target_file = "Tests/images/linear_gradient.png" target_file = "Tests/images/linear_gradient.png"
for mode in ["L", "P", "I", "F"]:
# Act # Act
im = Image.linear_gradient(mode) im = Image.linear_gradient(mode)
@ -565,11 +561,10 @@ class TestImage:
with pytest.raises(ValueError): with pytest.raises(ValueError):
Image.radial_gradient(wrong_mode) Image.radial_gradient(wrong_mode)
def test_radial_gradient(self): @pytest.mark.parametrize("mode", ("L", "P", "I", "F"))
def test_radial_gradient(self, mode):
# Arrange # Arrange
target_file = "Tests/images/radial_gradient.png" target_file = "Tests/images/radial_gradient.png"
for mode in ["L", "P", "I", "F"]:
# Act # Act
im = Image.radial_gradient(mode) im = Image.radial_gradient(mode)

View File

@ -184,8 +184,9 @@ class TestImageGetPixel(AccessTest):
with pytest.raises(error): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
def test_basic(self): @pytest.mark.parametrize(
for mode in ( "mode",
(
"1", "1",
"L", "L",
"LA", "LA",
@ -200,23 +201,28 @@ class TestImageGetPixel(AccessTest):
"RGBX", "RGBX",
"CMYK", "CMYK",
"YCbCr", "YCbCr",
): ),
)
def test_basic(self, mode):
self.check(mode) self.check(mode)
def test_signedness(self): @pytest.mark.parametrize("mode", ("I;16", "I;16B"))
def test_signedness(self, mode):
# see https://github.com/python-pillow/Pillow/issues/452 # see https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint* # pixelaccess is using signed int* instead of uint*
for mode in ("I;16", "I;16B"):
self.check(mode, 2**15 - 1) self.check(mode, 2**15 - 1)
self.check(mode, 2**15) self.check(mode, 2**15)
self.check(mode, 2**15 + 1) self.check(mode, 2**15 + 1)
self.check(mode, 2**16 - 1) self.check(mode, 2**16 - 1)
def test_p_putpixel_rgb_rgba(self): @pytest.mark.parametrize("mode", ("P", "PA"))
for color in [(255, 0, 0), (255, 0, 0, 255)]: @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
im = Image.new("P", (1, 1), 0) def test_p_putpixel_rgb_rgba(self, mode, color):
im = Image.new(mode, (1, 1))
im.putpixel((0, 0), color) im.putpixel((0, 0), color)
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
alpha = color[3] if len(color) == 4 and mode == "PA" else 255
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
@pytest.mark.skipif(cffi is None, reason="No CFFI") @pytest.mark.skipif(cffi is None, reason="No CFFI")
@ -337,12 +343,15 @@ class TestCffi(AccessTest):
# pixels can contain garbage if image is released # pixels can contain garbage if image is released
assert px[i, 0] == 0 assert px[i, 0] == 0
def test_p_putpixel_rgb_rgba(self): @pytest.mark.parametrize("mode", ("P", "PA"))
for color in [(255, 0, 0), (255, 0, 0, 255)]: def test_p_putpixel_rgb_rgba(self, mode):
im = Image.new("P", (1, 1), 0) for color in [(255, 0, 0), (255, 0, 0, 127)]:
im = Image.new(mode, (1, 1))
access = PyAccess.new(im, False) access = PyAccess.new(im, False)
access.putpixel((0, 0), color) access.putpixel((0, 0), color)
assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
alpha = color[3] if len(color) == 4 and mode == "PA" else 255
assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
class TestImagePutPixelError(AccessTest): class TestImagePutPixelError(AccessTest):

View File

@ -236,6 +236,12 @@ def test_p2pa_alpha():
assert im_a.getpixel((x, y)) == alpha assert im_a.getpixel((x, y)) == alpha
def test_p2pa_palette():
with Image.open("Tests/images/tiny.png") as im:
im_pa = im.convert("PA")
assert im_pa.getpalette() == im.getpalette()
def test_matrix_illegal_conversion(): def test_matrix_illegal_conversion():
# Arrange # Arrange
im = hopper("CMYK") im = hopper("CMYK")
@ -268,8 +274,8 @@ def test_matrix_wrong_mode():
im.convert(mode="L", matrix=matrix) im.convert(mode="L", matrix=matrix)
def test_matrix_xyz(): @pytest.mark.parametrize("mode", ("RGB", "L"))
def matrix_convert(mode): def test_matrix_xyz(mode):
# Arrange # Arrange
im = hopper("RGB") im = hopper("RGB")
im.info["transparency"] = (255, 0, 0) im.info["transparency"] = (255, 0, 0)
@ -296,9 +302,6 @@ def test_matrix_xyz():
assert_image_similar(converted_im, target.getchannel(0), 1) assert_image_similar(converted_im, target.getchannel(0), 1)
assert converted_im.info["transparency"] == 105 assert converted_im.info["transparency"] == 105
matrix_convert("RGB")
matrix_convert("L")
def test_matrix_identity(): def test_matrix_identity():
# Arrange # Arrange

View File

@ -1,14 +1,17 @@
import copy import copy
import pytest
from PIL import Image from PIL import Image
from .helper import hopper from .helper import hopper
def test_copy(): @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
def test_copy(mode):
cropped_coordinates = (10, 10, 20, 20) cropped_coordinates = (10, 10, 20, 20)
cropped_size = (10, 10) cropped_size = (10, 10)
for mode in "1", "P", "L", "RGB", "I", "F":
# Internal copy method # Internal copy method
im = hopper(mode) im = hopper(mode)
out = im.copy() out = im.copy()

View File

@ -5,8 +5,8 @@ from PIL import Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
def test_crop(): @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
def crop(mode): def test_crop(mode):
im = hopper(mode) im = hopper(mode)
assert_image_equal(im.crop(), im) assert_image_equal(im.crop(), im)
@ -14,9 +14,6 @@ def test_crop():
assert cropped.mode == mode assert cropped.mode == mode
assert cropped.size == (50, 50) assert cropped.size == (50, 50)
for mode in "1", "P", "L", "RGB", "I", "F":
crop(mode)
def test_wide_crop(): def test_wide_crop():
def crop(*bbox): def crop(*bbox):

View File

@ -5,53 +5,62 @@ from PIL import Image, ImageFilter
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
def test_sanity(): @pytest.mark.parametrize(
def apply_filter(filter_to_apply): "filter_to_apply",
for mode in ["L", "RGB", "CMYK"]: (
ImageFilter.BLUR,
ImageFilter.CONTOUR,
ImageFilter.DETAIL,
ImageFilter.EDGE_ENHANCE,
ImageFilter.EDGE_ENHANCE_MORE,
ImageFilter.EMBOSS,
ImageFilter.FIND_EDGES,
ImageFilter.SMOOTH,
ImageFilter.SMOOTH_MORE,
ImageFilter.SHARPEN,
ImageFilter.MaxFilter,
ImageFilter.MedianFilter,
ImageFilter.MinFilter,
ImageFilter.ModeFilter,
ImageFilter.GaussianBlur,
ImageFilter.GaussianBlur(5),
ImageFilter.BoxBlur(5),
ImageFilter.UnsharpMask,
ImageFilter.UnsharpMask(10),
),
)
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
def test_sanity(filter_to_apply, mode):
im = hopper(mode) im = hopper(mode)
out = im.filter(filter_to_apply) out = im.filter(filter_to_apply)
assert out.mode == im.mode assert out.mode == im.mode
assert out.size == im.size assert out.size == im.size
apply_filter(ImageFilter.BLUR)
apply_filter(ImageFilter.CONTOUR)
apply_filter(ImageFilter.DETAIL)
apply_filter(ImageFilter.EDGE_ENHANCE)
apply_filter(ImageFilter.EDGE_ENHANCE_MORE)
apply_filter(ImageFilter.EMBOSS)
apply_filter(ImageFilter.FIND_EDGES)
apply_filter(ImageFilter.SMOOTH)
apply_filter(ImageFilter.SMOOTH_MORE)
apply_filter(ImageFilter.SHARPEN)
apply_filter(ImageFilter.MaxFilter)
apply_filter(ImageFilter.MedianFilter)
apply_filter(ImageFilter.MinFilter)
apply_filter(ImageFilter.ModeFilter)
apply_filter(ImageFilter.GaussianBlur)
apply_filter(ImageFilter.GaussianBlur(5))
apply_filter(ImageFilter.BoxBlur(5))
apply_filter(ImageFilter.UnsharpMask)
apply_filter(ImageFilter.UnsharpMask(10))
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
def test_sanity_error(mode):
with pytest.raises(TypeError): with pytest.raises(TypeError):
apply_filter("hello") im = hopper(mode)
im.filter("hello")
def test_crash(): # crashes on small images
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
# crashes on small images def test_crash(size):
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", size)
im.filter(ImageFilter.SMOOTH)
im = Image.new("RGB", (2, 2))
im.filter(ImageFilter.SMOOTH)
im = Image.new("RGB", (3, 3))
im.filter(ImageFilter.SMOOTH) im.filter(ImageFilter.SMOOTH)
def test_modefilter(): @pytest.mark.parametrize(
def modefilter(mode): "mode, expected",
(
("1", (4, 0)),
("L", (4, 0)),
("P", (4, 0)),
("RGB", ((4, 0, 0), (0, 0, 0))),
),
)
def test_modefilter(mode, expected):
im = Image.new(mode, (3, 3), None) im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9))) im.putdata(list(range(9)))
# image is: # image is:
@ -61,16 +70,20 @@ def test_modefilter():
mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0 im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0]) # mode=0
mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1))
return mod, mod2 assert (mod, mod2) == expected
assert modefilter("1") == (4, 0)
assert modefilter("L") == (4, 0)
assert modefilter("P") == (4, 0)
assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0))
def test_rankfilter(): @pytest.mark.parametrize(
def rankfilter(mode): "mode, expected",
(
("1", (0, 4, 8)),
("L", (0, 4, 8)),
("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))),
("I", (0, 4, 8)),
("F", (0.0, 4.0, 8.0)),
),
)
def test_rankfilter(mode, expected):
im = Image.new(mode, (3, 3), None) im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9))) im.putdata(list(range(9)))
# image is: # image is:
@ -80,15 +93,21 @@ def test_rankfilter():
minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1))
med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1))
maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1))
return minimum, med, maximum assert (minimum, med, maximum) == expected
assert rankfilter("1") == (0, 4, 8)
assert rankfilter("L") == (0, 4, 8) @pytest.mark.parametrize(
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
)
def test_rankfilter_error(filter):
with pytest.raises(ValueError): with pytest.raises(ValueError):
rankfilter("P") im = Image.new("P", (3, 3), None)
assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0)) im.putdata(list(range(9)))
assert rankfilter("I") == (0, 4, 8) # image is:
assert rankfilter("F") == (0.0, 4.0, 8.0) # 0 1 2
# 3 4 5
# 6 7 8
im.filter(filter).getpixel((1, 1))
def test_rankfilter_properties(): def test_rankfilter_properties():
@ -110,7 +129,8 @@ def test_kernel_not_enough_coefficients():
ImageFilter.Kernel((3, 3), (0, 0)) ImageFilter.Kernel((3, 3), (0, 0))
def test_consistency_3x3(): @pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
def test_consistency_3x3(mode):
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss.bmp") as reference: with Image.open("Tests/images/hopper_emboss.bmp") as reference:
kernel = ImageFilter.Kernel( kernel = ImageFilter.Kernel(
@ -125,14 +145,14 @@ def test_consistency_3x3():
source = source.split() * 2 source = source.split() * 2
reference = reference.split() * 2 reference = reference.split() * 2
for mode in ["L", "LA", "RGB", "CMYK"]:
assert_image_equal( assert_image_equal(
Image.merge(mode, source[: len(mode)]).filter(kernel), Image.merge(mode, source[: len(mode)]).filter(kernel),
Image.merge(mode, reference[: len(mode)]), Image.merge(mode, reference[: len(mode)]),
) )
def test_consistency_5x5(): @pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
def test_consistency_5x5(mode):
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
kernel = ImageFilter.Kernel( kernel = ImageFilter.Kernel(
@ -149,7 +169,6 @@ def test_consistency_5x5():
source = source.split() * 2 source = source.split() * 2
reference = reference.split() * 2 reference = reference.split() * 2
for mode in ["L", "LA", "RGB", "CMYK"]:
assert_image_equal( assert_image_equal(
Image.merge(mode, source[: len(mode)]).filter(kernel), Image.merge(mode, source[: len(mode)]).filter(kernel),
Image.merge(mode, reference[: len(mode)]), Image.merge(mode, reference[: len(mode)]),

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image from PIL import Image
from .helper import CachedProperty, assert_image_equal from .helper import CachedProperty, assert_image_equal
@ -101,8 +103,8 @@ class TestImagingPaste:
], ],
) )
def test_image_solid(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_image_solid(self, mode):
im = Image.new(mode, (200, 200), "red") im = Image.new(mode, (200, 200), "red")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -111,8 +113,8 @@ class TestImagingPaste:
im = im.crop((12, 23, im2.width + 12, im2.height + 23)) im = im.crop((12, 23, im2.width + 12, im2.height + 23))
assert_image_equal(im, im2) assert_image_equal(im, im2)
def test_image_mask_1(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_image_mask_1(self, mode):
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -133,8 +135,8 @@ class TestImagingPaste:
], ],
) )
def test_image_mask_L(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_image_mask_L(self, mode):
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -155,8 +157,8 @@ class TestImagingPaste:
], ],
) )
def test_image_mask_LA(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_image_mask_LA(self, mode):
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -177,8 +179,8 @@ class TestImagingPaste:
], ],
) )
def test_image_mask_RGBA(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_image_mask_RGBA(self, mode):
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -199,8 +201,8 @@ class TestImagingPaste:
], ],
) )
def test_image_mask_RGBa(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_image_mask_RGBa(self, mode):
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -221,8 +223,8 @@ class TestImagingPaste:
], ],
) )
def test_color_solid(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_color_solid(self, mode):
im = Image.new(mode, (200, 200), "black") im = Image.new(mode, (200, 200), "black")
rect = (12, 23, 128 + 12, 128 + 23) rect = (12, 23, 128 + 12, 128 + 23)
@ -234,8 +236,8 @@ class TestImagingPaste:
assert head[255] == 128 * 128 assert head[255] == 128 * 128
assert sum(head[:255]) == 0 assert sum(head[:255]) == 0
def test_color_mask_1(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_color_mask_1(self, mode):
im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
color = (10, 20, 30, 40)[: len(mode)] color = (10, 20, 30, 40)[: len(mode)]
@ -256,8 +258,8 @@ class TestImagingPaste:
], ],
) )
def test_color_mask_L(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_color_mask_L(self, mode):
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"
@ -278,8 +280,8 @@ class TestImagingPaste:
], ],
) )
def test_color_mask_RGBA(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_color_mask_RGBA(self, mode):
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"
@ -300,8 +302,8 @@ class TestImagingPaste:
], ],
) )
def test_color_mask_RGBa(self): @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
for mode in ("RGBA", "RGB", "L"): def test_color_mask_RGBa(self, mode):
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"

View File

@ -38,58 +38,64 @@ gradients_image = Image.open("Tests/images/radial_gradients.png")
gradients_image.load() gradients_image.load()
def test_args_factor(): @pytest.mark.parametrize(
"size, expected",
(
(3, (4, 4)),
((3, 1), (4, 10)),
((1, 3), (10, 4)),
),
)
def test_args_factor(size, expected):
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
assert expected == im.reduce(size).size
assert (4, 4) == im.reduce(3).size
assert (4, 10) == im.reduce((3, 1)).size
assert (10, 4) == im.reduce((1, 3)).size
with pytest.raises(ValueError):
im.reduce(0)
with pytest.raises(TypeError):
im.reduce(2.0)
with pytest.raises(ValueError):
im.reduce((0, 10))
def test_args_box(): @pytest.mark.parametrize(
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
)
def test_args_factor_error(size, expected_error):
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size im.reduce(size)
assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size
with pytest.raises(TypeError):
im.reduce(2, "stri")
with pytest.raises(TypeError):
im.reduce(2, 2)
with pytest.raises(ValueError):
im.reduce(2, (0, 0, 11, 10))
with pytest.raises(ValueError):
im.reduce(2, (0, 0, 10, 11))
with pytest.raises(ValueError):
im.reduce(2, (-1, 0, 10, 10))
with pytest.raises(ValueError):
im.reduce(2, (0, -1, 10, 10))
with pytest.raises(ValueError):
im.reduce(2, (0, 5, 10, 5))
with pytest.raises(ValueError):
im.reduce(2, (5, 0, 5, 10))
def test_unsupported_modes(): @pytest.mark.parametrize(
"size, expected",
(
((0, 0, 10, 10), (5, 5)),
((5, 5, 6, 6), (1, 1)),
),
)
def test_args_box(size, expected):
im = Image.new("L", (10, 10))
assert expected == im.reduce(2, size).size
@pytest.mark.parametrize(
"size, expected_error",
(
("stri", TypeError),
((0, 0, 11, 10), ValueError),
((0, 0, 10, 11), ValueError),
((-1, 0, 10, 10), ValueError),
((0, -1, 10, 10), ValueError),
((0, 5, 10, 5), ValueError),
((5, 0, 5, 10), ValueError),
),
)
def test_args_box_error(size, expected_error):
im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
im.reduce(2, size).size
@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
def test_unsupported_modes(mode):
im = Image.new("P", (10, 10)) im = Image.new("P", (10, 10))
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.reduce(3) im.reduce(3)
im = Image.new("1", (10, 10))
with pytest.raises(ValueError):
im.reduce(3)
im = Image.new("I;16", (10, 10))
with pytest.raises(ValueError):
im.reduce(3)
def get_image(mode): def get_image(mode):
mode_info = ImageMode.getmode(mode) mode_info = ImageMode.getmode(mode)
@ -197,61 +203,67 @@ def test_mode_L():
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
def test_mode_LA(): @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_LA(factor):
im = get_image("LA") im = get_image("LA")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor, 0.8, 5) compare_reduce_with_reference(im, factor, 0.8, 5)
@pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_LA_opaque(factor):
im = get_image("LA")
# With opaque alpha, an error should be way smaller. # With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255)) im.putalpha(Image.new("L", im.size, 255))
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
def test_mode_La(): @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_La(factor):
im = get_image("La") im = get_image("La")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
def test_mode_RGB(): @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGB(factor):
im = get_image("RGB") im = get_image("RGB")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
def test_mode_RGBA(): @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGBA(factor):
im = get_image("RGBA") im = get_image("RGBA")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor, 0.8, 5) compare_reduce_with_reference(im, factor, 0.8, 5)
@pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGBA_opaque(factor):
im = get_image("RGBA")
# With opaque alpha, an error should be way smaller. # With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255)) im.putalpha(Image.new("L", im.size, 255))
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
def test_mode_RGBa(): @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGBa(factor):
im = get_image("RGBa") im = get_image("RGBa")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
def test_mode_I(): @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_I(factor):
im = get_image("I") im = get_image("I")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
def test_mode_F(): @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_F(factor):
im = get_image("F") im = get_image("F")
for factor in remarkable_factors:
compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_reference(im, factor, 0, 0)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)

View File

@ -100,8 +100,8 @@ class TestImagingCoreResampleAccuracy:
for y in range(image.size[1]) for y in range(image.size[1])
) )
def test_reduce_box(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_reduce_box(self, mode):
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX) case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off # fmt: off
@ -111,8 +111,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_reduce_bilinear(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_reduce_bilinear(self, mode):
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR) case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off # fmt: off
@ -122,8 +122,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_reduce_hamming(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_reduce_hamming(self, mode):
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING) case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off # fmt: off
@ -133,7 +133,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_reduce_bicubic(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_bicubic(self, mode):
for mode in ["RGBX", "RGB", "La", "L"]: for mode in ["RGBX", "RGB", "La", "L"]:
case = self.make_case(mode, (12, 12), 0xE1) case = self.make_case(mode, (12, 12), 0xE1)
case = case.resize((6, 6), Image.Resampling.BICUBIC) case = case.resize((6, 6), Image.Resampling.BICUBIC)
@ -145,8 +146,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (6, 6))) self.check_case(channel, self.make_sample(data, (6, 6)))
def test_reduce_lanczos(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_reduce_lanczos(self, mode):
case = self.make_case(mode, (16, 16), 0xE1) case = self.make_case(mode, (16, 16), 0xE1)
case = case.resize((8, 8), Image.Resampling.LANCZOS) case = case.resize((8, 8), Image.Resampling.LANCZOS)
# fmt: off # fmt: off
@ -158,8 +159,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (8, 8))) self.check_case(channel, self.make_sample(data, (8, 8)))
def test_enlarge_box(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_enlarge_box(self, mode):
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX) case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off # fmt: off
@ -169,8 +170,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_enlarge_bilinear(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_enlarge_bilinear(self, mode):
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR) case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off # fmt: off
@ -180,8 +181,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_enlarge_hamming(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_enlarge_hamming(self, mode):
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING) case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off # fmt: off
@ -191,8 +192,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
def test_enlarge_bicubic(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_enlarge_bicubic(self, mode):
case = self.make_case(mode, (4, 4), 0xE1) case = self.make_case(mode, (4, 4), 0xE1)
case = case.resize((8, 8), Image.Resampling.BICUBIC) case = case.resize((8, 8), Image.Resampling.BICUBIC)
# fmt: off # fmt: off
@ -204,8 +205,8 @@ class TestImagingCoreResampleAccuracy:
for channel in case.split(): for channel in case.split():
self.check_case(channel, self.make_sample(data, (8, 8))) self.check_case(channel, self.make_sample(data, (8, 8)))
def test_enlarge_lanczos(self): @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
for mode in ["RGBX", "RGB", "La", "L"]: def test_enlarge_lanczos(self, mode):
case = self.make_case(mode, (6, 6), 0xE1) case = self.make_case(mode, (6, 6), 0xE1)
case = case.resize((12, 12), Image.Resampling.LANCZOS) case = case.resize((12, 12), Image.Resampling.LANCZOS)
data = ( data = (
@ -419,16 +420,19 @@ class TestCoreResampleCoefficients:
class TestCoreResampleBox: class TestCoreResampleBox:
def test_wrong_arguments(self): @pytest.mark.parametrize(
im = hopper() "resample",
for resample in ( (
Image.Resampling.NEAREST, Image.Resampling.NEAREST,
Image.Resampling.BOX, Image.Resampling.BOX,
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
Image.Resampling.HAMMING, Image.Resampling.HAMMING,
Image.Resampling.BICUBIC, Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS, Image.Resampling.LANCZOS,
): ),
)
def test_wrong_arguments(self, resample):
im = hopper()
im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (0, 0, im.width, im.height))
im.resize((32, 32), resample, (20, 20, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height))
im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 20, 100))
@ -509,9 +513,11 @@ class TestCoreResampleBox:
with pytest.raises(AssertionError, match=r"difference 29\."): with pytest.raises(AssertionError, match=r"difference 29\."):
assert_image_similar(reference, without_box, 5) assert_image_similar(reference, without_box, 5)
def test_formats(self): @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", ""))
for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]: @pytest.mark.parametrize(
for mode in ["RGB", "L", "RGBA", "LA", "I", ""]: "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR)
)
def test_formats(self, mode, resample):
im = hopper(mode) im = hopper(mode)
box = (20, 20, im.size[0] - 20, im.size[1] - 20) box = (20, 20, im.size[0] - 20, im.size[1] - 20)
with_box = im.resize((32, 32), resample, box) with_box = im.resize((32, 32), resample, box)

View File

@ -22,19 +22,10 @@ class TestImagingCoreResize:
im.load() im.load()
return im._new(im.im.resize(size, f)) return im._new(im.im.resize(size, f))
def test_nearest_mode(self): @pytest.mark.parametrize(
for mode in [ "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16")
"1", )
"P", def test_nearest_mode(self, mode):
"L",
"I",
"F",
"RGB",
"RGBA",
"CMYK",
"YCbCr",
"I;16",
]: # exotic mode
im = hopper(mode) im = hopper(mode)
r = self.resize(im, (15, 12), Image.Resampling.NEAREST) r = self.resize(im, (15, 12), Image.Resampling.NEAREST)
assert r.mode == mode assert r.mode == mode
@ -55,33 +46,58 @@ class TestImagingCoreResize:
assert r.size == (15, 12) assert r.size == (15, 12)
assert r.im.bands == im.im.bands assert r.im.bands == im.im.bands
def test_reduce_filters(self): @pytest.mark.parametrize(
for f in [ "resample",
(
Image.Resampling.NEAREST, Image.Resampling.NEAREST,
Image.Resampling.BOX, Image.Resampling.BOX,
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
Image.Resampling.HAMMING, Image.Resampling.HAMMING,
Image.Resampling.BICUBIC, Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS, Image.Resampling.LANCZOS,
]: ),
r = self.resize(hopper("RGB"), (15, 12), f) )
def test_reduce_filters(self, resample):
r = self.resize(hopper("RGB"), (15, 12), resample)
assert r.mode == "RGB" assert r.mode == "RGB"
assert r.size == (15, 12) assert r.size == (15, 12)
def test_enlarge_filters(self): @pytest.mark.parametrize(
for f in [ "resample",
(
Image.Resampling.NEAREST, Image.Resampling.NEAREST,
Image.Resampling.BOX, Image.Resampling.BOX,
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
Image.Resampling.HAMMING, Image.Resampling.HAMMING,
Image.Resampling.BICUBIC, Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS, Image.Resampling.LANCZOS,
]: ),
r = self.resize(hopper("RGB"), (212, 195), f) )
def test_enlarge_filters(self, resample):
r = self.resize(hopper("RGB"), (212, 195), resample)
assert r.mode == "RGB" assert r.mode == "RGB"
assert r.size == (212, 195) assert r.size == (212, 195)
def test_endianness(self): @pytest.mark.parametrize(
"resample",
(
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
),
)
@pytest.mark.parametrize(
"mode, channels_set",
(
("RGB", ("blank", "filled", "dirty")),
("RGBA", ("blank", "blank", "filled", "dirty")),
("LA", ("filled", "dirty")),
),
)
def test_endianness(self, resample, mode, channels_set):
# Make an image with one colored pixel, in one channel. # Make an image with one colored pixel, in one channel.
# When resized, that channel should be the same as a GS image. # When resized, that channel should be the same as a GS image.
# Other channels should be unaffected. # Other channels should be unaffected.
@ -95,44 +111,34 @@ class TestImagingCoreResize:
} }
samples["dirty"].putpixel((1, 1), 128) samples["dirty"].putpixel((1, 1), 128)
for f in [
Image.Resampling.NEAREST,
Image.Resampling.BOX,
Image.Resampling.BILINEAR,
Image.Resampling.HAMMING,
Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS,
]:
# samples resized with current filter # samples resized with current filter
references = { references = {
name: self.resize(ch, (4, 4), f) for name, ch in samples.items() name: self.resize(ch, (4, 4), resample) for name, ch in samples.items()
} }
for mode, channels_set in [
("RGB", ("blank", "filled", "dirty")),
("RGBA", ("blank", "blank", "filled", "dirty")),
("LA", ("filled", "dirty")),
]:
for channels in set(permutations(channels_set)): for channels in set(permutations(channels_set)):
# compile image from different channels permutations # compile image from different channels permutations
im = Image.merge(mode, [samples[ch] for ch in channels]) im = Image.merge(mode, [samples[ch] for ch in channels])
resized = self.resize(im, (4, 4), f) resized = self.resize(im, (4, 4), resample)
for i, ch in enumerate(resized.split()): for i, ch in enumerate(resized.split()):
# check what resized channel in image is the same # check what resized channel in image is the same
# as separately resized channel # as separately resized channel
assert_image_equal(ch, references[channels[i]]) assert_image_equal(ch, references[channels[i]])
def test_enlarge_zero(self): @pytest.mark.parametrize(
for f in [ "resample",
(
Image.Resampling.NEAREST, Image.Resampling.NEAREST,
Image.Resampling.BOX, Image.Resampling.BOX,
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
Image.Resampling.HAMMING, Image.Resampling.HAMMING,
Image.Resampling.BICUBIC, Image.Resampling.BICUBIC,
Image.Resampling.LANCZOS, Image.Resampling.LANCZOS,
]: ),
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) )
def test_enlarge_zero(self, resample):
r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample)
assert r.mode == "RGB" assert r.mode == "RGB"
assert r.size == (212, 195) assert r.size == (212, 195)
assert r.getdata()[0] == (0, 0, 0) assert r.getdata()[0] == (0, 0, 0)
@ -179,12 +185,11 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99
) )
def test_reducing_gap_1(self, gradients_image): @pytest.mark.parametrize(
for box, epsilon in [ "box, epsilon",
(None, 4), ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)),
((1.1, 2.2, 510.8, 510.9), 4), )
((3, 10, 410, 256), 10), def test_reducing_gap_1(self, gradients_image, box, epsilon):
]:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
@ -195,12 +200,11 @@ class TestReducingGapResize:
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_2(self, gradients_image): @pytest.mark.parametrize(
for box, epsilon in [ "box, epsilon",
(None, 1.5), ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)),
((1.1, 2.2, 510.8, 510.9), 1.5), )
((3, 10, 410, 256), 1), def test_reducing_gap_2(self, gradients_image, box, epsilon):
]:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
@ -211,12 +215,11 @@ class TestReducingGapResize:
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_3(self, gradients_image): @pytest.mark.parametrize(
for box, epsilon in [ "box, epsilon",
(None, 1), ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)),
((1.1, 2.2, 510.8, 510.9), 1), )
((3, 10, 410, 256), 0.5), def test_reducing_gap_3(self, gradients_image, box, epsilon):
]:
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
@ -227,8 +230,8 @@ class TestReducingGapResize:
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_8(self, gradients_image): @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)))
for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: def test_reducing_gap_8(self, gradients_image, box):
ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0
@ -236,11 +239,11 @@ class TestReducingGapResize:
assert_image_equal(ref, im) assert_image_equal(ref, im)
def test_box_filter(self, gradients_image): @pytest.mark.parametrize(
for box, epsilon in [ "box, epsilon",
((0, 0, 512, 512), 5.5), (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)),
((0.9, 1.7, 128, 128), 9.5), )
]: def test_box_filter(self, gradients_image, box, epsilon):
ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box)
im = gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0
@ -273,15 +276,14 @@ class TestImageResize:
im = im.resize((64, 64)) im = im.resize((64, 64))
assert im.size == (64, 64) assert im.size == (64, 64)
def test_default_filter(self): @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F"))
for mode in "L", "RGB", "I", "F": def test_default_filter_bicubic(self, mode):
im = hopper(mode) im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20))
for mode in "1", "P": @pytest.mark.parametrize(
im = hopper(mode) "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16")
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) )
def test_default_filter_nearest(self, mode):
for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16":
im = hopper(mode) im = hopper(mode)
assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20))

View File

@ -1,3 +1,5 @@
import pytest
from PIL import Image from PIL import Image
from .helper import ( from .helper import (
@ -22,14 +24,14 @@ def rotate(im, mode, angle, center=None, translate=None):
assert out.size != im.size assert out.size != im.size
def test_mode(): @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
for mode in ("1", "P", "L", "RGB", "I", "F"): def test_mode(mode):
im = hopper(mode) im = hopper(mode)
rotate(im, mode, 45) rotate(im, mode, 45)
def test_angle(): @pytest.mark.parametrize("angle", (0, 90, 180, 270))
for angle in (0, 90, 180, 270): def test_angle(angle):
with Image.open("Tests/images/test-card.png") as im: with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle) rotate(im, im.mode, angle)
@ -37,8 +39,8 @@ def test_angle():
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
def test_zero(): @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
for angle in (0, 45, 90, 180, 270): def test_zero(angle):
im = Image.new("RGB", (0, 0)) im = Image.new("RGB", (0, 0))
rotate(im, im.mode, angle) rotate(im, im.mode, angle)

View File

@ -97,6 +97,28 @@ def test_load_first():
im.thumbnail((64, 64)) im.thumbnail((64, 64))
assert im.size == (64, 10) assert im.size == (64, 10)
# Test thumbnail(), without draft(),
# on an image that is large enough once load() has changed the size
with Image.open("Tests/images/g4_orientation_5.tif") as im:
im.thumbnail((590, 88), reducing_gap=None)
assert im.size == (590, 88)
def test_load_first_unless_jpeg():
# Test that thumbnail() still uses draft() for JPEG
with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft
def im_draft(mode, size):
result = draft(mode, size)
assert result is not None
return result
im.draft = im_draft
im.thumbnail((64, 64))
# valgrind test is failing with memory allocated in libjpeg # valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing") @pytest.mark.valgrind_known_error(reason="Known Failing")

View File

@ -75,12 +75,15 @@ class TestImageTransform:
assert_image_equal(transformed, scaled) assert_image_equal(transformed, scaled)
def test_fill(self): @pytest.mark.parametrize(
for mode, pixel in [ "mode, expected_pixel",
["RGB", (255, 0, 0)], (
["RGBA", (255, 0, 0, 255)], ("RGB", (255, 0, 0)),
["LA", (76, 0)], ("RGBA", (255, 0, 0, 255)),
]: ("LA", (76, 0)),
),
)
def test_fill(self, mode, expected_pixel):
im = hopper(mode) im = hopper(mode)
(w, h) = im.size (w, h) = im.size
transformed = im.transform( transformed = im.transform(
@ -90,8 +93,7 @@ class TestImageTransform:
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
fillcolor="red", fillcolor="red",
) )
assert transformed.getpixel((w - 1, h - 1)) == expected_pixel
assert transformed.getpixel((w - 1, h - 1)) == pixel
def test_mesh(self): def test_mesh(self):
# this should be a checkerboard of halfsized hoppers in ul, lr # this should be a checkerboard of halfsized hoppers in ul, lr
@ -222,14 +224,12 @@ class TestImageTransform:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.transform((100, 100), None) im.transform((100, 100), None)
def test_unknown_resampling_filter(self): @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
def test_unknown_resampling_filter(self, resample):
with hopper() as im: with hopper() as im:
(w, h) = im.size (w, h) = im.size
for resample in (Image.Resampling.BOX, "unknown"):
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.transform( im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample)
(100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample
)
class TestImageTransformAffine: class TestImageTransformAffine:
@ -239,7 +239,16 @@ class TestImageTransformAffine:
im = hopper("RGB") im = hopper("RGB")
return im.crop((10, 20, im.width - 10, im.height - 20)) return im.crop((10, 20, im.width - 10, im.height - 20))
def _test_rotate(self, deg, transpose): @pytest.mark.parametrize(
"deg, transpose",
(
(0, None),
(90, Image.Transpose.ROTATE_90),
(180, Image.Transpose.ROTATE_180),
(270, Image.Transpose.ROTATE_270),
),
)
def test_rotate(self, deg, transpose):
im = self._test_image() im = self._test_image()
angle = -math.radians(deg) angle = -math.radians(deg)
@ -271,77 +280,65 @@ class TestImageTransformAffine:
) )
assert_image_equal(transposed, transformed) assert_image_equal(transposed, transformed)
def test_rotate_0_deg(self): @pytest.mark.parametrize(
self._test_rotate(0, None) "scale, epsilon_scale",
(
def test_rotate_90_deg(self): (1.1, 6.9),
self._test_rotate(90, Image.Transpose.ROTATE_90) (1.5, 5.5),
(2.0, 5.5),
def test_rotate_180_deg(self): (2.3, 3.7),
self._test_rotate(180, Image.Transpose.ROTATE_180) (2.5, 3.7),
),
def test_rotate_270_deg(self): )
self._test_rotate(270, Image.Transpose.ROTATE_270) @pytest.mark.parametrize(
"resample,epsilon",
def _test_resize(self, scale, epsilonscale): (
(Image.Resampling.NEAREST, 0),
(Image.Resampling.BILINEAR, 2),
(Image.Resampling.BICUBIC, 1),
),
)
def test_resize(self, scale, epsilon_scale, resample, epsilon):
im = self._test_image() im = self._test_image()
size_up = int(round(im.width * scale)), int(round(im.height * scale)) size_up = int(round(im.width * scale)), int(round(im.height * scale))
matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0]
matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0]
for resample, epsilon in [
(Image.Resampling.NEAREST, 0),
(Image.Resampling.BILINEAR, 2),
(Image.Resampling.BICUBIC, 1),
]:
transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = im.transform(size_up, self.transform, matrix_up, resample)
transformed = transformed.transform( transformed = transformed.transform(
im.size, self.transform, matrix_down, resample im.size, self.transform, matrix_down, resample
) )
assert_image_similar(transformed, im, epsilon * epsilonscale) assert_image_similar(transformed, im, epsilon * epsilon_scale)
def test_resize_1_1x(self): @pytest.mark.parametrize(
self._test_resize(1.1, 6.9) "x, y, epsilon_scale",
(
def test_resize_1_5x(self): (0.1, 0, 3.7),
self._test_resize(1.5, 5.5) (0.6, 0, 9.1),
(50, 50, 0),
def test_resize_2_0x(self): ),
self._test_resize(2.0, 5.5) )
@pytest.mark.parametrize(
def test_resize_2_3x(self): "resample, epsilon",
self._test_resize(2.3, 3.7) (
(Image.Resampling.NEAREST, 0),
def test_resize_2_5x(self): (Image.Resampling.BILINEAR, 1.5),
self._test_resize(2.5, 3.7) (Image.Resampling.BICUBIC, 1),
),
def _test_translate(self, x, y, epsilonscale): )
def test_translate(self, x, y, epsilon_scale, resample, epsilon):
im = self._test_image() im = self._test_image()
size_up = int(round(im.width + x)), int(round(im.height + y)) size_up = int(round(im.width + x)), int(round(im.height + y))
matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] matrix_up = [1, 0, -x, 0, 1, -y, 0, 0]
matrix_down = [1, 0, x, 0, 1, y, 0, 0] matrix_down = [1, 0, x, 0, 1, y, 0, 0]
for resample, epsilon in [
(Image.Resampling.NEAREST, 0),
(Image.Resampling.BILINEAR, 1.5),
(Image.Resampling.BICUBIC, 1),
]:
transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = im.transform(size_up, self.transform, matrix_up, resample)
transformed = transformed.transform( transformed = transformed.transform(
im.size, self.transform, matrix_down, resample im.size, self.transform, matrix_down, resample
) )
assert_image_similar(transformed, im, epsilon * epsilonscale) assert_image_similar(transformed, im, epsilon * epsilon_scale)
def test_translate_0_1(self):
self._test_translate(0.1, 0, 3.7)
def test_translate_0_6(self):
self._test_translate(0.6, 0, 9.1)
def test_translate_50(self):
self._test_translate(50, 50, 0)
class TestImageTransformPerspective(TestImageTransformAffine): class TestImageTransformPerspective(TestImageTransformAffine):

View File

@ -1,3 +1,5 @@
import pytest
from PIL.Image import Transpose from PIL.Image import Transpose
from . import helper from . import helper
@ -9,8 +11,8 @@ HOPPER = {
} }
def test_flip_left_right(): @pytest.mark.parametrize("mode", HOPPER)
def transpose(mode): def test_flip_left_right(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(Transpose.FLIP_LEFT_RIGHT) out = im.transpose(Transpose.FLIP_LEFT_RIGHT)
assert out.mode == mode assert out.mode == mode
@ -22,12 +24,9 @@ def test_flip_left_right():
assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2))
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2))
for mode in HOPPER:
transpose(mode)
@pytest.mark.parametrize("mode", HOPPER)
def test_flip_top_bottom(): def test_flip_top_bottom(mode):
def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(Transpose.FLIP_TOP_BOTTOM) out = im.transpose(Transpose.FLIP_TOP_BOTTOM)
assert out.mode == mode assert out.mode == mode
@ -39,12 +38,9 @@ def test_flip_top_bottom():
assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1))
for mode in HOPPER:
transpose(mode)
@pytest.mark.parametrize("mode", HOPPER)
def test_rotate_90(): def test_rotate_90(mode):
def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(Transpose.ROTATE_90) out = im.transpose(Transpose.ROTATE_90)
assert out.mode == mode assert out.mode == mode
@ -56,12 +52,9 @@ def test_rotate_90():
assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2))
assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1))
for mode in HOPPER:
transpose(mode)
@pytest.mark.parametrize("mode", HOPPER)
def test_rotate_180(): def test_rotate_180(mode):
def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(Transpose.ROTATE_180) out = im.transpose(Transpose.ROTATE_180)
assert out.mode == mode assert out.mode == mode
@ -73,12 +66,9 @@ def test_rotate_180():
assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1))
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
for mode in HOPPER:
transpose(mode)
@pytest.mark.parametrize("mode", HOPPER)
def test_rotate_270(): def test_rotate_270(mode):
def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(Transpose.ROTATE_270) out = im.transpose(Transpose.ROTATE_270)
assert out.mode == mode assert out.mode == mode
@ -90,12 +80,9 @@ def test_rotate_270():
assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) assert im.getpixel((1, y - 2)) == out.getpixel((1, 1))
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2))
for mode in HOPPER:
transpose(mode)
@pytest.mark.parametrize("mode", HOPPER)
def test_transpose(): def test_transpose(mode):
def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(Transpose.TRANSPOSE) out = im.transpose(Transpose.TRANSPOSE)
assert out.mode == mode assert out.mode == mode
@ -107,12 +94,9 @@ def test_transpose():
assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1))
assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2))
for mode in HOPPER:
transpose(mode)
@pytest.mark.parametrize("mode", HOPPER)
def test_tranverse(): def test_tranverse(mode):
def transpose(mode):
im = HOPPER[mode] im = HOPPER[mode]
out = im.transpose(Transpose.TRANSVERSE) out = im.transpose(Transpose.TRANSVERSE)
assert out.mode == mode assert out.mode == mode
@ -124,12 +108,9 @@ def test_tranverse():
assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2))
assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1))
for mode in HOPPER:
transpose(mode)
@pytest.mark.parametrize("mode", HOPPER)
def test_roundtrip(): def test_roundtrip(mode):
for mode in HOPPER:
im = HOPPER[mode] im = HOPPER[mode]
def transpose(first, second): def transpose(first, second):

View File

@ -625,10 +625,10 @@ def test_polygon2():
helper_polygon(POINTS2) helper_polygon(POINTS2)
def test_polygon_kite(): @pytest.mark.parametrize("mode", ("RGB", "L"))
def test_polygon_kite(mode):
# Test drawing lines of different gradients (dx>dy, dy>dx) and # Test drawing lines of different gradients (dx>dy, dy>dx) and
# vertical (dx==0) and horizontal (dy==0) lines # vertical (dx==0) and horizontal (dy==0) lines
for mode in ["RGB", "L"]:
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -1314,6 +1314,23 @@ def test_stroke_multiline():
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3)
def test_setting_default_font():
# Arrange
im = Image.new("RGB", (100, 250))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
ImageDraw.ImageDraw.font = font
# Assert
try:
assert draw.getfont() == font
finally:
ImageDraw.ImageDraw.font = None
assert isinstance(draw.getfont(), ImageFont.ImageFont)
def test_same_color_outline(): def test_same_color_outline():
# Prepare shape # Prepare shape
x0, y0 = 5, 5 x0, y0 = 5, 5

File diff suppressed because it is too large Load Diff

View File

@ -345,12 +345,16 @@ def test_exif_transpose():
check(orientation_im) check(orientation_im)
# Orientation from "XML:com.adobe.xmp" info key # Orientation from "XML:com.adobe.xmp" info key
with Image.open("Tests/images/xmp_tags_orientation.png") as im: for suffix in ("", "_exiftool"):
with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im:
assert im.getexif()[0x0112] == 3 assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif()
assert 0x0112 not in transposed_im.getexif()
# Orientation from "Raw profile type exif" info key # Orientation from "Raw profile type exif" info key
# This test image has been manually hexedited from exif_imagemagick.png # This test image has been manually hexedited from exif_imagemagick.png
# to have a different orientation # to have a different orientation

View File

@ -16,8 +16,8 @@ if ImageQt.qt_is_installed:
from PIL.ImageQt import QImage from PIL.ImageQt import QImage
def test_sanity(tmp_path): @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
for mode in ("RGB", "RGBA", "L", "P", "1"): def test_sanity(mode, tmp_path):
src = hopper(mode) src = hopper(mode)
data = ImageQt.toqimage(src) data = ImageQt.toqimage(src)
@ -32,12 +32,12 @@ def test_sanity(tmp_path):
assert_image_equal(rt, src) assert_image_equal(rt, src)
if mode == "1": if mode == "1":
# BW appears to not save correctly on QT4 and QT5 # BW appears to not save correctly on QT5
# kicks out errors on console: # kicks out errors on console:
# libpng warning: Invalid color type/bit depth combination # libpng warning: Invalid color type/bit depth combination
# in IHDR # in IHDR
# libpng error: Invalid IHDR data # libpng error: Invalid IHDR data
continue return
# Test saving the file # Test saving the file
tempfile = str(tmp_path / f"temp_{mode}.png") tempfile = str(tmp_path / f"temp_{mode}.png")

View File

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

View File

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

View File

@ -837,6 +837,24 @@ Pillow reads and writes TGA images containing ``L``, ``LA``, ``P``,
``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and ``RGB``, and ``RGBA`` data. Pillow can read and write both uncompressed and
run-length encoded TGAs. run-length encoded TGAs.
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**compression**
If set to "tga_rle", the file will be run-length encoded.
.. versionadded:: 5.3.0
**id_section**
The identification field.
.. versionadded:: 5.3.0
**orientation**
If present and a positive number, the first pixel is for the top left corner,
rather than the bottom left corner.
.. versionadded:: 5.3.0
TIFF TIFF
^^^^ ^^^^
@ -968,7 +986,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``, methods are: :data:`None`, ``"group3"``, ``"group4"``, ``"jpeg"``, ``"lzma"``,
``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``, ``"packbits"``, ``"tiff_adobe_deflate"``, ``"tiff_ccitt"``, ``"tiff_lzw"``,
``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``, ``"tiff_raw_16"``, ``"tiff_sgilog"``, ``"tiff_sgilog24"``, ``"tiff_thunderscan"``,
``"webp"`, ``"zstd"`` ``"webp"``, ``"zstd"``
**quality** **quality**
The image quality for JPEG compression, on a scale from 0 (worst) to 100 The image quality for JPEG compression, on a scale from 0 (worst) to 100
@ -1209,6 +1227,17 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL
methods may be used to read other pictures from the file. The pictures are methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported. zero-indexed and random access is supported.
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
option will also be available.
**append_images**
A list of images to append as additional pictures. Each of the
images in the list can be single or multiframe images.
.. versionadded:: 9.3.0
PCD PCD
^^^ ^^^

View File

@ -15,35 +15,13 @@ Python Support
Pillow supports these Python versions. Pillow supports these Python versions.
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ .. csv-table:: Newer versions
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 | :file: newer-versions.csv
+======================+=====+=====+=====+=====+=====+=====+=====+=====+ :header-rows: 1
| Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ .. csv-table:: Older versions
| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 | :file: older-versions.csv
+==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+ :header-rows: 1
| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
Basic Installation Basic Installation
------------------ ------------------
@ -188,7 +166,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.0** * Pillow has been tested with libimagequant **2.6-4.0.4**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.
@ -389,7 +367,7 @@ In Alpine, the command is::
.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
Prerequisites for **Ubuntu 16.04 LTS - 20.04 LTS** are installed with:: Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \

6
docs/newer-versions.csv Normal file
View File

@ -0,0 +1,6 @@
Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5
Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,
Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,
Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,
Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes
1 Python 3.11 3.10 3.9 3.8 3.7 3.6 3.5
2 Pillow >= 9.3 Yes Yes Yes Yes Yes
3 Pillow 9.0 - 9.2 Yes Yes Yes Yes
4 Pillow 8.3.2 - 8.4 Yes Yes Yes Yes Yes
5 Pillow 8.0 - 8.3.1 Yes Yes Yes Yes
6 Pillow 7.0 - 7.2 Yes Yes Yes Yes

8
docs/older-versions.csv Normal file
View File

@ -0,0 +1,8 @@
Python,3.8,3.7,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4
Pillow 6.2.1 - 6.2.2,Yes,Yes,Yes,Yes,,,,Yes,,,
Pillow 6.0 - 6.2.0,,Yes,Yes,Yes,,,,Yes,,,
Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,,
Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,,
Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,,
Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,,
Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes
1 Python 3.8 3.7 3.6 3.5 3.4 3.3 3.2 2.7 2.6 2.5 2.4
2 Pillow 6.2.1 - 6.2.2 Yes Yes Yes Yes Yes
3 Pillow 6.0 - 6.2.0 Yes Yes Yes Yes
4 Pillow 5.2 - 5.4 Yes Yes Yes Yes Yes
5 Pillow 5.0 - 5.1 Yes Yes Yes Yes
6 Pillow 4 Yes Yes Yes Yes Yes
7 Pillow 2 - 3 Yes Yes Yes Yes Yes Yes
8 Pillow < 2 Yes Yes Yes Yes

View File

@ -53,9 +53,9 @@ Functions
To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files
which decompress into a huge amount of data and are designed to crash or cause disruption by using up which decompress into a huge amount of data and are designed to crash or cause disruption by using up
a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the number of pixels in an
image is over a certain limit, :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. image is over a certain limit, :py:data:`MAX_IMAGE_PIXELS`.
This threshold can be changed by setting :py:data:`PIL.Image.MAX_IMAGE_PIXELS`. It can be disabled This threshold can be changed by setting :py:data:`MAX_IMAGE_PIXELS`. It can be disabled
by setting ``Image.MAX_IMAGE_PIXELS = None``. by setting ``Image.MAX_IMAGE_PIXELS = None``.
If desired, the warning can be turned into an error with If desired, the warning can be turned into an error with
@ -63,7 +63,7 @@ Functions
``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also
`the logging documentation`_ to have warnings output to the logging facility instead of stderr. `the logging documentation`_ to have warnings output to the logging facility instead of stderr.
If the number of pixels is greater than twice :py:data:`PIL.Image.MAX_IMAGE_PIXELS`, then a If the number of pixels is greater than twice :py:data:`MAX_IMAGE_PIXELS`, then a
``DecompressionBombError`` will be raised instead. ``DecompressionBombError`` will be raised instead.
.. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb
@ -255,7 +255,7 @@ This rotates the input image by ``theta`` degrees counter clockwise:
.. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transform
.. automethod:: PIL.Image.Image.transpose .. automethod:: PIL.Image.Image.transpose
This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT` This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT`
method. method.
.. code-block:: python .. code-block:: python

View File

@ -64,7 +64,7 @@ Fonts
PIL can use bitmap fonts or OpenType/TrueType fonts. PIL can use bitmap fonts or OpenType/TrueType fonts.
Bitmap fonts are stored in PILs own format, where each font typically consists Bitmap fonts are stored in PIL's own format, where each font typically consists
of two files, one named .pil and the other usually named .pbm. The former of two files, one named .pil and the other usually named .pbm. The former
contains font metrics, the latter raster data. contains font metrics, the latter raster data.
@ -146,6 +146,11 @@ Methods
Get the current default font. Get the current default font.
To set the default font for all future ImageDraw instances::
from PIL import ImageDraw, ImageFont
ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
:returns: An image font. :returns: An image font.
.. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) .. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0)

View File

@ -73,7 +73,7 @@ Access using negative indexes is also possible.
Modifies the pixel at x,y. The color is given as a single Modifies the pixel at x,y. The color is given as a single
numerical value for single band images, and a tuple for numerical value for single band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples multi-band images. In addition to this, RGB and RGBA tuples
are accepted for P images. are accepted for P and PA images.
:param xy: The pixel coordinate, given as (x, y). :param xy: The pixel coordinate, given as (x, y).
:param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode) :param color: The pixel value according to its mode. e.g. tuple (r, g, b) for RGB mode)

View File

@ -0,0 +1,69 @@
9.3.0
-----
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
TODO
^^^^
TODO
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
Allow default ImageDraw font to be set
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Rather than specifying a font when calling text-related ImageDraw methods, or
setting a font on each ImageDraw instance, the default font can now be set for
all future ImageDraw operations::
from PIL import ImageDraw, ImageFont
ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
Saving multiple MPO frames
^^^^^^^^^^^^^^^^^^^^^^^^^^
Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of
an image's frames will be saved to file::
from PIL import Image
im = Image.open("frozenpond.mpo")
im.save(out, save_all=True)
Additional images can also be appended when saving, by combining the
``save_all`` argument with the ``append_images`` argument::
im.save(out, save_all=True, append_images=[im1, im2, ...])
Security
========
TODO
^^^^
TODO
Other Changes
=============
Added DDS ATI1 and ATI2 reading
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support has been added to read the ATI1 and ATI2 formats of DDS images.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
9.3.0
9.2.0 9.2.0
9.1.1 9.1.1
9.1.0 9.1.0

View File

@ -34,7 +34,11 @@ project_urls =
Twitter=https://twitter.com/PythonPillow Twitter=https://twitter.com/PythonPillow
[options] [options]
packages = PIL
python_requires = >=3.7 python_requires = >=3.7
include_package_data = True
package_dir =
= src
[options.extras_require] [options.extras_require]
docs = docs =

View File

@ -15,7 +15,9 @@ import subprocess
import sys import sys
import warnings import warnings
from setuptools import Extension, setup from setuptools import Extension
from setuptools import __version__ as setuptools_version
from setuptools import setup
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
@ -850,6 +852,7 @@ class pil_build_ext(build_ext):
sys.platform == "win32" sys.platform == "win32"
and sys.version_info < (3, 9) and sys.version_info < (3, 9)
and not (PLATFORM_PYPY or PLATFORM_MINGW) and not (PLATFORM_PYPY or PLATFORM_MINGW)
and int(setuptools_version.split(".")[0]) < 60
): ):
defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""'))
else: else:
@ -996,9 +999,6 @@ try:
version=PILLOW_VERSION, version=PILLOW_VERSION,
cmdclass={"build_ext": pil_build_ext}, cmdclass={"build_ext": pil_build_ext},
ext_modules=ext_modules, ext_modules=ext_modules,
include_package_data=True,
packages=["PIL"],
package_dir={"": "src"},
zip_safe=not (debug_build() or PLATFORM_MINGW), zip_safe=not (debug_build() or PLATFORM_MINGW),
) )
except RequiredDependencyException as err: except RequiredDependencyException as err:

View File

@ -375,6 +375,16 @@ def _save(im, fp, filename, bitmap_header=True):
header = 40 # or 64 for OS/2 version 2 header = 40 # or 64 for OS/2 version 2
image = stride * im.size[1] image = stride * im.size[1]
if im.mode == "1":
palette = b"".join(o8(i) * 4 for i in (0, 255))
elif im.mode == "L":
palette = b"".join(o8(i) * 4 for i in range(256))
elif im.mode == "P":
palette = im.im.getpalette("RGB", "BGRX")
colors = len(palette) // 4
else:
palette = None
# bitmap header # bitmap header
if bitmap_header: if bitmap_header:
offset = 14 + header + colors * 4 offset = 14 + header + colors * 4
@ -405,14 +415,8 @@ def _save(im, fp, filename, bitmap_header=True):
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
if im.mode == "1": if palette:
for i in (0, 255): fp.write(palette)
fp.write(o8(i) * 4)
elif im.mode == "L":
for i in range(256):
fp.write(o8(i) * 4)
elif im.mode == "P":
fp.write(im.im.getpalette("RGB", "BGRX"))
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])

View File

@ -156,6 +156,14 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"DXT5": elif fourcc == b"DXT5":
self.pixel_format = "DXT5" self.pixel_format = "DXT5"
n = 3 n = 3
elif fourcc == b"ATI1":
self.pixel_format = "BC4"
n = 4
self.mode = "L"
elif fourcc == b"ATI2":
self.pixel_format = "BC5"
n = 5
self.mode = "RGB"
elif fourcc == b"BC5S": elif fourcc == b"BC5S":
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5

View File

@ -288,12 +288,15 @@ class EpsImageFile(ImageFile.ImageFile):
# Encoded bitmapped image. # Encoded bitmapped image.
x, y, bi, mo = s[11:].split(None, 7)[:4] x, y, bi, mo = s[11:].split(None, 7)[:4]
if int(bi) != 8: if int(bi) == 1:
break self.mode = "1"
elif int(bi) == 8:
try: try:
self.mode = self.mode_map[int(mo)] self.mode = self.mode_map[int(mo)]
except ValueError: except ValueError:
break break
else:
break
self._size = int(x), int(y) self._size = int(x), int(y)
return return

View File

@ -185,8 +185,6 @@ class GifImageFile(ImageFile.ImageFile):
if not s or s == b";": if not s or s == b";":
raise EOFError raise EOFError
self.tile = []
palette = None palette = None
info = {} info = {}
@ -295,6 +293,8 @@ class GifImageFile(ImageFile.ImageFile):
if not update_image: if not update_image:
return return
self.tile = []
if self.dispose: if self.dispose:
self.im.paste(self.dispose, self.dispose_extent) self.im.paste(self.dispose, self.dispose_extent)

View File

@ -1404,9 +1404,9 @@ class Image:
if 0x0112 not in self._exif: if 0x0112 not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp") xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags: if xmp_tags:
match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match: if match:
self._exif[0x0112] = int(match[1]) self._exif[0x0112] = int(match[2])
return self._exif return self._exif
@ -1839,7 +1839,7 @@ class Image:
Modifies the pixel at the given position. The color is given as Modifies the pixel at the given position. The color is given as
a single numerical value for single-band images, and a tuple for a single numerical value for single-band images, and a tuple for
multi-band images. In addition to this, RGB and RGBA tuples are multi-band images. In addition to this, RGB and RGBA tuples are
accepted for P images. accepted for P and PA images.
Note that this method is relatively slow. For more extensive changes, Note that this method is relatively slow. For more extensive changes,
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
@ -1864,12 +1864,17 @@ class Image:
return self.pyaccess.putpixel(xy, value) return self.pyaccess.putpixel(xy, value)
if ( if (
self.mode == "P" self.mode in ("P", "PA")
and isinstance(value, (list, tuple)) and isinstance(value, (list, tuple))
and len(value) in [3, 4] and len(value) in [3, 4]
): ):
# RGB or RGBA value for a P image # RGB or RGBA value for a P or PA image
if self.mode == "PA":
alpha = value[3] if len(value) == 4 else 255
value = value[:3]
value = self.palette.getcolor(value, self) value = self.palette.getcolor(value, self)
if self.mode == "PA":
value = (value, alpha)
return self.im.putpixel(xy, value) return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None): def remap_palette(self, dest_map, source_palette=None):
@ -1984,18 +1989,14 @@ class Image:
:param size: The requested size in pixels, as a 2-tuple: :param size: The requested size in pixels, as a 2-tuple:
(width, height). (width, height).
:param resample: An optional resampling filter. This can be :param resample: An optional resampling filter. This can be
one of :py:data:`PIL.Image.Resampling.NEAREST`, one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
:py:data:`PIL.Image.Resampling.BOX`, :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
:py:data:`PIL.Image.Resampling.BILINEAR`, :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
: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 If the image has mode "1" or "P", it is always set to
:py:data:`PIL.Image.Resampling.NEAREST`. :py:data:`Resampling.NEAREST`. If the image mode specifies a number
If the image mode specifies a number of bits, such as "I;16", then the of bits, such as "I;16", then the default filter is
default filter is :py:data:`PIL.Image.Resampling.NEAREST`. :py:data:`Resampling.NEAREST`. Otherwise, the default filter is
Otherwise, the default filter is :py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`.
:py:data:`PIL.Image.Resampling.BICUBIC`. See: :ref:`concept-filters`.
:param box: An optional 4-tuple of floats providing :param box: An optional 4-tuple of floats providing
the source image region to be scaled. the source image region to be scaled.
The values must be within (0, 0, width, height) rectangle. The values must be within (0, 0, width, height) rectangle.
@ -2135,12 +2136,12 @@ class Image:
:param angle: In degrees counter clockwise. :param angle: In degrees counter clockwise.
:param resample: An optional resampling filter. This can be :param resample: An optional resampling filter. This can be
one of :py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), one of :py:data:`Resampling.NEAREST` (use nearest neighbour),
:py:data:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2
environment), or :py:data:`PIL.Image.Resampling.BICUBIC` environment), or :py:data:`Resampling.BICUBIC` (cubic spline
(cubic spline interpolation in a 4x4 environment). interpolation in a 4x4 environment). If omitted, or if the image has
If omitted, or if the image has mode "1" or "P", it is mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`.
set to :py:data:`PIL.Image.Resampling.NEAREST`. See :ref:`concept-filters`. See :ref:`concept-filters`.
:param expand: Optional expansion flag. If true, expands the output :param expand: Optional expansion flag. If true, expands the output
image to make it large enough to hold the entire rotated image. 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 If false or omitted, make the output image the same size as the
@ -2447,14 +2448,11 @@ class Image:
:param size: Requested size. :param size: Requested size.
:param resample: Optional resampling filter. This can be one :param resample: Optional resampling filter. This can be one
of :py:data:`PIL.Image.Resampling.NEAREST`, of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
:py:data:`PIL.Image.Resampling.BOX`, :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`,
:py:data:`PIL.Image.Resampling.BILINEAR`, :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`.
:py:data:`PIL.Image.Resampling.HAMMING`, If omitted, it defaults to :py:data:`Resampling.BICUBIC`.
:py:data:`PIL.Image.Resampling.BICUBIC` or (was :py:data:`Resampling.NEAREST` prior to version 2.5.0).
: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`. See: :ref:`concept-filters`.
:param reducing_gap: Apply optimization by resizing the image :param reducing_gap: Apply optimization by resizing the image
in two steps. First, reducing the image by integer times in two steps. First, reducing the image by integer times
@ -2473,15 +2471,16 @@ class Image:
:returns: None :returns: None
""" """
self.load() provided_size = tuple(map(math.floor, size))
x, y = map(math.floor, size)
if x >= self.width and y >= self.height:
return
def preserve_aspect_ratio():
def round_aspect(number, key): def round_aspect(number, key):
return max(min(math.floor(number), math.ceil(number), key=key), 1) return max(min(math.floor(number), math.ceil(number), key=key), 1)
# preserve aspect ratio x, y = provided_size
if x >= self.width and y >= self.height:
return
aspect = self.width / self.height aspect = self.width / self.height
if x / y >= aspect: if x / y >= aspect:
x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y))
@ -2489,13 +2488,24 @@ class Image:
y = round_aspect( y = round_aspect(
x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n)
) )
size = (x, y) return x, y
box = None box = None
if reducing_gap is not None: if reducing_gap is not None:
size = preserve_aspect_ratio()
if size is None:
return
res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap))
if res is not None: if res is not None:
box = res[1] box = res[1]
if box is None:
self.load()
# load() may have changed the size of the image
size = preserve_aspect_ratio()
if size is None:
return
if self.size != size: if self.size != size:
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
@ -2525,11 +2535,11 @@ class Image:
:param size: The output size. :param size: The output size.
:param method: The transformation method. This is one of :param method: The transformation method. This is one of
:py:data:`PIL.Image.Transform.EXTENT` (cut out a rectangular subregion), :py:data:`Transform.EXTENT` (cut out a rectangular subregion),
:py:data:`PIL.Image.Transform.AFFINE` (affine transform), :py:data:`Transform.AFFINE` (affine transform),
:py:data:`PIL.Image.Transform.PERSPECTIVE` (perspective transform), :py:data:`Transform.PERSPECTIVE` (perspective transform),
:py:data:`PIL.Image.Transform.QUAD` (map a quadrilateral to a rectangle), or :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or
:py:data:`PIL.Image.Transform.MESH` (map a number of source quadrilaterals :py:data:`Transform.MESH` (map a number of source quadrilaterals
in one operation). in one operation).
It may also be an :py:class:`~PIL.Image.ImageTransformHandler` It may also be an :py:class:`~PIL.Image.ImageTransformHandler`
@ -2549,11 +2559,11 @@ class Image:
return method, data return method, data
:param data: Extra data to the transformation method. :param data: Extra data to the transformation method.
:param resample: Optional resampling filter. It can be one of :param resample: Optional resampling filter. It can be one of
:py:data:`PIL.Image.Resampling.NEAREST` (use nearest neighbour), :py:data:`Resampling.NEAREST` (use nearest neighbour),
:py:data:`PIL.Image.Resampling.BILINEAR` (linear interpolation in a 2x2 :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2
environment), or :py:data:`PIL.Image.BICUBIC` (cubic spline environment), or :py:data:`Resampling.BICUBIC` (cubic spline
interpolation in a 4x4 environment). If omitted, or if the image interpolation in a 4x4 environment). If omitted, or if the image
has mode "1" or "P", it is set to :py:data:`PIL.Image.Resampling.NEAREST`. has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`.
See: :ref:`concept-filters`. See: :ref:`concept-filters`.
:param fill: If ``method`` is an :param fill: If ``method`` is an
:py:class:`~PIL.Image.ImageTransformHandler` object, this is one of :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of
@ -2680,13 +2690,10 @@ class Image:
""" """
Transpose image (flip or rotate in 90 degree steps) Transpose image (flip or rotate in 90 degree steps)
:param method: One of :py:data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT`, :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`,
:py:data:`PIL.Image.Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`,
:py:data:`PIL.Image.Transpose.ROTATE_90`, :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`,
:py:data:`PIL.Image.Transpose.ROTATE_180`, :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`.
: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. :returns: Returns a flipped or rotated copy of this image.
""" """

View File

@ -46,6 +46,8 @@ directly.
class ImageDraw: class ImageDraw:
font = None
def __init__(self, im, mode=None): def __init__(self, im, mode=None):
""" """
Create a drawing instance. Create a drawing instance.
@ -86,12 +88,16 @@ class ImageDraw:
else: else:
self.fontmode = "L" # aliasing is okay for other modes self.fontmode = "L" # aliasing is okay for other modes
self.fill = 0 self.fill = 0
self.font = None
def getfont(self): def getfont(self):
""" """
Get the current default font. Get the current default font.
To set the default font for all future ImageDraw instances::
from PIL import ImageDraw, ImageFont
ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
:returns: An image font.""" :returns: An image font."""
if not self.font: if not self.font:
# FIXME: should add a font repository # FIXME: should add a font repository

View File

@ -192,6 +192,9 @@ class ImageFile(Image.Image):
with open(self.filename) as fp: with open(self.filename) as fp:
self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
if offset + self.size[1] * args[1] > self.map.size():
# buffer is not large enough
raise OSError
self.im = Image.core.map_buffer( self.im = Image.core.map_buffer(
self.map, self.size, decoder_name, offset, args self.map, self.size, decoder_name, offset, args
) )
@ -499,9 +502,14 @@ def _save(im, fp, tile, bufsize=0):
try: try:
fh = fp.fileno() fh = fp.fileno()
fp.flush() fp.flush()
exc = None _encode_tile(im, fp, tile, bufsize, fh)
except (AttributeError, io.UnsupportedOperation) as e: except (AttributeError, io.UnsupportedOperation) as exc:
exc = e _encode_tile(im, fp, tile, bufsize, None, exc)
if hasattr(fp, "flush"):
fp.flush()
def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
for e, b, o, a in tile: for e, b, o, a in tile:
if o > 0: if o > 0:
fp.seek(o) fp.seek(o)
@ -526,8 +534,6 @@ def _save(im, fp, tile, bufsize=0):
raise OSError(f"encoder error {s} when writing image file") from exc raise OSError(f"encoder error {s} when writing image file") from exc
finally: finally:
encoder.cleanup() encoder.cleanup()
if hasattr(fp, "flush"):
fp.flush()
def _safe_read(fp, size): def _safe_read(fp, size):

View File

@ -906,10 +906,12 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
This function loads a font object from the given file or file-like This function loads a font object from the given file or file-like
object, and creates a font object for a font of the given size. object, and creates a font object for a font of the given size.
Pillow uses FreeType to open font files. If you are opening many fonts Pillow uses FreeType to open font files. On Windows, be aware that FreeType
simultaneously on Windows, be aware that Windows limits the number of files will keep the file open as long as the FreeTypeFont object exists. Windows
that can be open in C at once to 512. If you approach that limit, an limits the number of files that can be open in C at once to 512, so if many
fonts are opened simultaneously and that limit is approached, an
``OSError`` may be thrown, reporting that FreeType "cannot open resource". ``OSError`` may be thrown, reporting that FreeType "cannot open resource".
A workaround would be to copy the file(s) into memory, and open that instead.
This function requires the _imagingft service. This function requires the _imagingft service.

View File

@ -572,8 +572,11 @@ def solarize(image, threshold=128):
def exif_transpose(image): def exif_transpose(image):
""" """
If an image has an EXIF Orientation tag, return a new image that is If an image has an EXIF Orientation tag, other than 1, return a new image
transposed accordingly. Otherwise, return a copy of the image. that is transposed accordingly. The new image will have the orientation
data removed.
Otherwise, return a copy of the image.
:param image: The image to transpose. :param image: The image to transpose.
:return: An image. :return: An image.
@ -601,10 +604,12 @@ def exif_transpose(image):
"Raw profile type exif" "Raw profile type exif"
] = transposed_exif.tobytes().hex() ] = transposed_exif.tobytes().hex()
elif "XML:com.adobe.xmp" in transposed_image.info: elif "XML:com.adobe.xmp" in transposed_image.info:
transposed_image.info["XML:com.adobe.xmp"] = re.sub( for pattern in (
r'tiff:Orientation="([0-9])"', r'tiff:Orientation="([0-9])"',
"", r"<tiff:Orientation>([0-9])</tiff:Orientation>",
transposed_image.info["XML:com.adobe.xmp"], ):
transposed_image.info["XML:com.adobe.xmp"] = re.sub(
pattern, "", transposed_image.info["XML:com.adobe.xmp"]
) )
return transposed_image return transposed_image
return image.copy() return image.copy()

View File

@ -68,21 +68,7 @@ def _pyimagingtkcall(command, photo, id):
# may raise an error if it cannot attach to Tkinter # may raise an error if it cannot attach to Tkinter
from . import _imagingtk from . import _imagingtk
try: _imagingtk.tkinit(tk.interpaddr())
if hasattr(tk, "interp"):
# Required for PyPy, which always has CFFI installed
from cffi import FFI
ffi = FFI()
# PyPy is using an FFI CDATA element
# (Pdb) self.tk.interp
# <cdata 'Tcl_Interp *' 0x3061b50>
_imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1)
else:
_imagingtk.tkinit(tk.interpaddr(), 1)
except AttributeError:
_imagingtk.tkinit(id(tk), 0)
tk.call(command, photo, id) tk.call(command, photo, id)

View File

@ -711,7 +711,7 @@ def _save(im, fp, filename):
qtables = getattr(im, "quantization", None) qtables = getattr(im, "quantization", None)
qtables = validate_qtables(qtables) qtables = validate_qtables(qtables)
extra = b"" extra = info.get("extra", b"")
icc_profile = info.get("icc_profile") icc_profile = info.get("icc_profile")
if icc_profile: if icc_profile:

View File

@ -18,16 +18,66 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from . import Image, ImageFile, JpegImagePlugin import itertools
import os
import struct
from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import o32le
# def _accept(prefix): # def _accept(prefix):
# return JpegImagePlugin._accept(prefix) # return JpegImagePlugin._accept(prefix)
def _save(im, fp, filename): def _save(im, fp, filename):
# Note that we can only save the current frame at present JpegImagePlugin._save(im, fp, filename)
return JpegImagePlugin._save(im, fp, filename)
def _save_all(im, fp, filename):
append_images = im.encoderinfo.get("append_images", [])
if not append_images:
try:
animated = im.is_animated
except AttributeError:
animated = False
if not animated:
_save(im, fp, filename)
return
offsets = []
for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence):
if not offsets:
# APP2 marker
im.encoderinfo["extra"] = (
b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70
)
JpegImagePlugin._save(im_frame, fp, filename)
offsets.append(fp.tell())
else:
im_frame.save(fp, "JPEG")
offsets.append(fp.tell() - offsets[-1])
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[0xB001] = len(offsets)
mpentries = b""
data_offset = 0
for i, size in enumerate(offsets):
if i == 0:
mptype = 0x030000 # Baseline MP Primary Image
else:
mptype = 0x000000 # Undefined
mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
if i == 0:
data_offset -= 28
data_offset += size
ifd[0xB002] = mpentries
fp.seek(28)
fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8))
fp.seek(0, os.SEEK_END)
## ##
@ -124,6 +174,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
# Image.register_open(MpoImageFile.format, # Image.register_open(MpoImageFile.format,
# JpegImagePlugin.jpeg_factory, _accept) # JpegImagePlugin.jpeg_factory, _accept)
Image.register_save(MpoImageFile.format, _save) Image.register_save(MpoImageFile.format, _save)
Image.register_save_all(MpoImageFile.format, _save_all)
Image.register_extension(MpoImageFile.format, ".mpo") Image.register_extension(MpoImageFile.format, ".mpo")

View File

@ -21,10 +21,11 @@
## ##
import io import io
import math
import os import os
import time import time
from . import Image, ImageFile, ImageSequence, PdfParser, __version__ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -123,7 +124,28 @@ def _save(im, fp, filename, save_all=False):
params = None params = None
decode = None decode = None
#
# Get image characteristics
width, height = im.size
if im.mode == "1": if im.mode == "1":
if features.check("libtiff"):
filter = "CCITTFaxDecode"
bits = 1
params = PdfParser.PdfArray(
[
PdfParser.PdfDict(
{
"K": -1,
"BlackIs1": True,
"Columns": width,
"Rows": height,
}
)
]
)
else:
filter = "DCTDecode" filter = "DCTDecode"
colorspace = PdfParser.PdfName("DeviceGray") colorspace = PdfParser.PdfName("DeviceGray")
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
@ -161,6 +183,14 @@ def _save(im, fp, filename, save_all=False):
if filter == "ASCIIHexDecode": if filter == "ASCIIHexDecode":
ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
elif filter == "CCITTFaxDecode":
im.save(
op,
"TIFF",
compression="group4",
# use a single strip
strip_size=math.ceil(im.width / 8) * im.height,
)
elif filter == "DCTDecode": elif filter == "DCTDecode":
Image.SAVE["JPEG"](im, op, filename) Image.SAVE["JPEG"](im, op, filename)
elif filter == "FlateDecode": elif filter == "FlateDecode":
@ -170,22 +200,24 @@ def _save(im, fp, filename, save_all=False):
else: else:
raise ValueError(f"unsupported PDF filter ({filter})") raise ValueError(f"unsupported PDF filter ({filter})")
# stream = op.getvalue()
# Get image characteristics if filter == "CCITTFaxDecode":
stream = stream[8:]
width, height = im.size filter = PdfParser.PdfArray([PdfParser.PdfName(filter)])
else:
filter = PdfParser.PdfName(filter)
existing_pdf.write_obj( existing_pdf.write_obj(
image_refs[page_number], image_refs[page_number],
stream=op.getvalue(), stream=stream,
Type=PdfParser.PdfName("XObject"), Type=PdfParser.PdfName("XObject"),
Subtype=PdfParser.PdfName("Image"), Subtype=PdfParser.PdfName("Image"),
Width=width, # * 72.0 / resolution, Width=width, # * 72.0 / resolution,
Height=height, # * 72.0 / resolution, Height=height, # * 72.0 / resolution,
Filter=PdfParser.PdfName(filter), Filter=filter,
BitsPerComponent=bits, BitsPerComponent=bits,
Decode=decode, Decode=decode,
DecodeParams=params, DecodeParms=params,
ColorSpace=colorspace, ColorSpace=colorspace,
) )

View File

@ -75,6 +75,9 @@ class PsdImageFile(ImageFile.ImageFile):
if channels > psd_channels: if channels > psd_channels:
raise OSError("not enough channels") raise OSError("not enough channels")
if mode == "RGB" and psd_channels == 4:
mode = "RGBA"
channels = 4
self.mode = mode self.mode = mode
self._size = i32(s, 18), i32(s, 14) self._size = i32(s, 18), i32(s, 14)

View File

@ -58,7 +58,7 @@ class PyAccess:
# Keep pointer to im object to prevent dereferencing. # Keep pointer to im object to prevent dereferencing.
self._im = img.im self._im = img.im
if self._im.mode == "P": if self._im.mode in ("P", "PA"):
self._palette = img.palette self._palette = img.palette
# Debugging is polluting test traces, only useful here # Debugging is polluting test traces, only useful here
@ -89,12 +89,17 @@ class PyAccess:
(x, y) = self.check_xy((x, y)) (x, y) = self.check_xy((x, y))
if ( if (
self._im.mode == "P" self._im.mode in ("P", "PA")
and isinstance(color, (list, tuple)) and isinstance(color, (list, tuple))
and len(color) in [3, 4] and len(color) in [3, 4]
): ):
# RGB or RGBA value for a P image # RGB or RGBA value for a P or PA image
if self._im.mode == "PA":
alpha = color[3] if len(color) == 4 else 255
color = color[:3]
color = self._palette.getcolor(color, self._img) color = self._palette.getcolor(color, self._img)
if self._im.mode == "PA":
color = (color, alpha)
return self.set_pixel(x, y, color) return self.set_pixel(x, y, color)

View File

@ -193,9 +193,10 @@ def _save(im, fp, filename):
warnings.warn("id_section has been trimmed to 255 characters") warnings.warn("id_section has been trimmed to 255 characters")
if colormaptype: if colormaptype:
colormapfirst, colormaplength, colormapentry = 0, 256, 24 palette = im.im.getpalette("RGB", "BGR")
colormaplength, colormapentry = len(palette) // 3, 24
else: else:
colormapfirst, colormaplength, colormapentry = 0, 0, 0 colormaplength, colormapentry = 0, 0
if im.mode in ("LA", "RGBA"): if im.mode in ("LA", "RGBA"):
flags = 8 flags = 8
@ -210,7 +211,7 @@ def _save(im, fp, filename):
o8(id_len) o8(id_len)
+ o8(colormaptype) + o8(colormaptype)
+ o8(imagetype) + o8(imagetype)
+ o16(colormapfirst) + o16(0) # colormapfirst
+ o16(colormaplength) + o16(colormaplength)
+ o8(colormapentry) + o8(colormapentry)
+ o16(0) + o16(0)
@ -225,7 +226,7 @@ def _save(im, fp, filename):
fp.write(id_section) fp.write(id_section)
if colormaptype: if colormaptype:
fp.write(im.im.getpalette("RGB", "BGR")) fp.write(palette)
if rle: if rle:
ImageFile._save( ImageFile._save(

View File

@ -727,7 +727,9 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(2) @_register_writer(2)
def write_string(self, value): def write_string(self, value):
# remerge of https://github.com/python-pillow/Pillow/pull/1416 # remerge of https://github.com/python-pillow/Pillow/pull/1416
return b"" + value.encode("ascii", "replace") + b"\0" if not isinstance(value, bytes):
value = value.encode("ascii", "replace")
return value + b"\0"
@_register_loader(5, 8) @_register_loader(5, 8)
def load_rational(self, data, legacy_api=True): def load_rational(self, data, legacy_api=True):
@ -1153,7 +1155,7 @@ class TiffImageFile(ImageFile.ImageFile):
:returns: XMP tags in a dictionary. :returns: XMP tags in a dictionary.
""" """
return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}
def get_photoshop_blocks(self): def get_photoshop_blocks(self):
""" """
@ -1328,7 +1330,7 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug(f"- photometric_interpretation: {photo}") logger.debug(f"- photometric_interpretation: {photo}")
logger.debug(f"- planar_configuration: {self._planar_configuration}") logger.debug(f"- planar_configuration: {self._planar_configuration}")
logger.debug(f"- fill_order: {fillorder}") logger.debug(f"- fill_order: {fillorder}")
logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}") logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")
# size # size
xsize = int(self.tag_v2.get(IMAGEWIDTH)) xsize = int(self.tag_v2.get(IMAGEWIDTH))
@ -1469,8 +1471,8 @@ class TiffImageFile(ImageFile.ImageFile):
else: else:
# tiled image # tiled image
offsets = self.tag_v2[TILEOFFSETS] offsets = self.tag_v2[TILEOFFSETS]
w = self.tag_v2.get(322) w = self.tag_v2.get(TILEWIDTH)
h = self.tag_v2.get(323) h = self.tag_v2.get(TILELENGTH)
for offset in offsets: for offset in offsets:
if x + w > xsize: if x + w > xsize:
@ -1684,7 +1686,8 @@ def _save(im, fp, filename):
stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
# aim for given strip size (64 KB by default) when using libtiff writer # aim for given strip size (64 KB by default) when using libtiff writer
if libtiff: if libtiff:
rows_per_strip = 1 if stride == 0 else min(STRIP_SIZE // stride, im.size[1]) im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
# JPEG encoder expects multiple of 8 rows # JPEG encoder expects multiple of 8 rows
if compression == "jpeg": if compression == "jpeg":
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])

View File

@ -23,33 +23,16 @@ TkImaging_Init(Tcl_Interp *interp);
extern int extern int
load_tkinter_funcs(void); load_tkinter_funcs(void);
/* copied from _tkinter.c (this isn't as bad as it may seem: for new
versions, we use _tkinter's interpaddr hook instead, and all older
versions use this structure layout) */
typedef struct {
PyObject_HEAD Tcl_Interp *interp;
} TkappObject;
static PyObject * static PyObject *
_tkinit(PyObject *self, PyObject *args) { _tkinit(PyObject *self, PyObject *args) {
Tcl_Interp *interp; Tcl_Interp *interp;
PyObject *arg; PyObject *arg;
int is_interp; if (!PyArg_ParseTuple(args, "O", &arg)) {
if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) {
return NULL; return NULL;
} }
if (is_interp) {
interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg); interp = (Tcl_Interp *)PyLong_AsVoidPtr(arg);
} else {
TkappObject *app;
/* Do it the hard way. This will break if the TkappObject
layout changes */
app = (TkappObject *)PyLong_AsVoidPtr(arg);
interp = app->interp;
}
/* This will bomb if interp is invalid... */ /* This will bomb if interp is invalid... */
TkImaging_Init(interp); TkImaging_Init(interp);

View File

@ -1026,6 +1026,14 @@ pa2l(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
} }
} }
static void
pa2p(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x;
for (x = 0; x < xsize; x++, in += 4) {
*out++ = in[0];
}
}
static void static void
p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) { p2pa(UINT8 *out, const UINT8 *in, int xsize, ImagingPalette palette) {
int x; int x;
@ -1209,6 +1217,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
convert = alpha ? pa2l : p2l; convert = alpha ? pa2l : p2l;
} else if (strcmp(mode, "LA") == 0) { } else if (strcmp(mode, "LA") == 0) {
convert = alpha ? pa2la : p2la; convert = alpha ? pa2la : p2la;
} else if (strcmp(mode, "P") == 0) {
convert = pa2p;
} else if (strcmp(mode, "PA") == 0) { } else if (strcmp(mode, "PA") == 0) {
convert = p2pa; convert = p2pa;
} else if (strcmp(mode, "I") == 0) { } else if (strcmp(mode, "I") == 0) {
@ -1233,6 +1243,10 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) {
if (!imOut) { if (!imOut) {
return NULL; return NULL;
} }
if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) {
ImagingPaletteDelete(imOut->palette);
imOut->palette = ImagingPaletteDuplicate(imIn->palette);
}
ImagingSectionEnter(&cookie); ImagingSectionEnter(&cookie);
for (y = 0; y < imIn->ysize; y++) { for (y = 0; y < imIn->ysize; y++) {

View File

@ -916,7 +916,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
dump_state(clientstate); dump_state(clientstate);
if (state->state == 0) { if (state->state == 0) {
TRACE(("Encoding line bt line")); TRACE(("Encoding line by line"));
while (state->y < state->ysize) { while (state->y < state->ysize) {
state->shuffle( state->shuffle(
state->buffer, state->buffer,

View File

@ -11,8 +11,8 @@ For more extensive info, see the [Windows build instructions](build.rst).
* Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires Microsoft Visual Studio 2017 or newer with C++ component.
* Requires NASM for libjpeg-turbo, a required dependency when using this script. * Requires NASM for libjpeg-turbo, a required dependency when using this script.
* Requires CMake 3.12 or newer (available as Visual Studio component). * Requires CMake 3.12 or newer (available as Visual Studio component).
* Tested on Windows Server 2016 with Visual Studio 2017 Community (AppVeyor). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor).
* Tested on Windows Server 2019 with Visual Studio 2019 Enterprise (GitHub Actions). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions).
The following is a simplified version of the script used on AppVeyor: The following is a simplified version of the script used on AppVeyor:
``` ```

View File

@ -108,9 +108,9 @@ header = [
deps = { deps = {
"libjpeg": { "libjpeg": {
"url": SF_PROJECTS "url": SF_PROJECTS
+ "/libjpeg-turbo/files/2.1.3/libjpeg-turbo-2.1.3.tar.gz/download", + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download",
"filename": "libjpeg-turbo-2.1.3.tar.gz", "filename": "libjpeg-turbo-2.1.4.tar.gz",
"dir": "libjpeg-turbo-2.1.3", "dir": "libjpeg-turbo-2.1.4",
"build": [ "build": [
cmd_cmake( cmd_cmake(
[ [
@ -157,9 +157,9 @@ deps = {
# "bins": [r"libtiff\*.dll"], # "bins": [r"libtiff\*.dll"],
}, },
"libwebp": { "libwebp": {
"url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.3.tar.gz", "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz",
"filename": "libwebp-1.2.3.tar.gz", "filename": "libwebp-1.2.4.tar.gz",
"dir": "libwebp-1.2.3", "dir": "libwebp-1.2.4",
"build": [ "build": [
cmd_rmdir(r"output\release-static"), # clean cmd_rmdir(r"output\release-static"), # clean
cmd_nmake( cmd_nmake(
@ -226,21 +226,21 @@ deps = {
"filename": "lcms2-2.13.1.tar.gz", "filename": "lcms2-2.13.1.tar.gz",
"dir": "lcms2-2.13.1", "dir": "lcms2-2.13.1",
"patch": { "patch": {
r"Projects\VC2019\lcms2_static\lcms2_static.vcxproj": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
# default is /MD for x86 and /MT for x64, we need /MD always # default is /MD for x86 and /MT for x64, we need /MD always
"<RuntimeLibrary>MultiThreaded</RuntimeLibrary>": "<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>", # noqa: E501 "<RuntimeLibrary>MultiThreaded</RuntimeLibrary>": "<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>", # noqa: E501
# retarget to default toolset (selected by vcvarsall.bat) # retarget to default toolset (selected by vcvarsall.bat)
"<PlatformToolset>v142</PlatformToolset>": "<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>", # noqa: E501 "<PlatformToolset>v143</PlatformToolset>": "<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>", # noqa: E501
# retarget to latest (selected by vcvarsall.bat) # retarget to latest (selected by vcvarsall.bat)
"<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>": "<WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>", # noqa: E501 "<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>": "<WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>", # noqa: E501
} }
}, },
"build": [ "build": [
cmd_rmdir("Lib"), cmd_rmdir("Lib"),
cmd_rmdir(r"Projects\VC2019\Release"), cmd_rmdir(r"Projects\VC2022\Release"),
cmd_msbuild(r"Projects\VC2019\lcms2.sln", "Release", "Clean"), cmd_msbuild(r"Projects\VC2022\lcms2.sln", "Release", "Clean"),
cmd_msbuild( cmd_msbuild(
r"Projects\VC2019\lcms2.sln", "Release", "lcms2_static:Rebuild" r"Projects\VC2022\lcms2.sln", "Release", "lcms2_static:Rebuild"
), ),
cmd_xcopy("include", "{inc_dir}"), cmd_xcopy("include", "{inc_dir}"),
], ],
@ -281,9 +281,9 @@ deps = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.1.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip",
"filename": "harfbuzz-4.4.1.zip", "filename": "harfbuzz-5.1.0.zip",
"dir": "harfbuzz-4.4.1", "dir": "harfbuzz-5.1.0",
"build": [ "build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"), cmd_nmake(target="clean"),