Merge branch 'main' into apng

This commit is contained in:
Andrew Murray 2023-06-14 11:26:42 +10:00
commit 17b19b5668
64 changed files with 843 additions and 444 deletions

View File

@ -22,7 +22,8 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard
fi
python3 -m pip install --upgrade pip
@ -41,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6
fi

View File

@ -13,10 +13,6 @@ indent_style = space
trim_trailing_whitespace = true
[*.rst]
# Four-space indentation
indent_size = 4
[*.yml]
# Two-space indentation
indent_size = 2

View File

@ -39,6 +39,7 @@ jobs:
centos-stream-8-amd64,
centos-stream-9-amd64,
debian-11-bullseye-x86,
debian-12-bookworm-x86,
fedora-37-amd64,
fedora-38-amd64,
gentoo,

View File

@ -65,8 +65,8 @@ jobs:
- name: Print build system information
run: python3 .github/workflows/system-info.py
- name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
- name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml
run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml
- name: Install dependencies
id: install

View File

@ -84,7 +84,9 @@ jobs:
python3 -m pip install pytest-reverse
fi
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
xvfb-run -s '-screen 0 1024x768x24' sway&
export WAYLAND_DISPLAY=wayland-1
.ci/test.sh
else
.ci/test.sh
fi

View File

@ -4,9 +4,6 @@ repos:
hooks:
- id: black
args: [--target-version=py38]
# Only .py files, until https://github.com/psf/black/issues/402 resolved
files: \.py$
types: []
- repo: https://github.com/PyCQA/isort
rev: 5.12.0

View File

@ -1,6 +1,6 @@
version: 2
formats: all
formats: [pdf]
build:
os: ubuntu-22.04

View File

@ -5,6 +5,57 @@ Changelog (Pillow)
10.0.0 (unreleased)
-------------------
- Fixed combining single duration across duplicate APNG frames #7146
[radarhere]
- Remove temporary file when error is raised #7148
[radarhere]
- Do not use temporary file when grabbing clipboard on Linux #7200
[radarhere]
- If the clipboard fails to open on Windows, wait and try again #7141
[radarhere]
- Fixed saving multiple 1 mode frames to GIF #7181
[radarhere]
- Replaced absolute PIL import with relative import #7173
[radarhere]
- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192
[radarhere]
- Improved wl-paste mimetype handling in ImageGrab #7094
[rrcgat, radarhere]
- Added _repr_jpeg_() for IPython display_jpeg #7135
[n3011, radarhere, nulano]
- Use "/sbin/ldconfig" if ldconfig is not found #7068
[radarhere]
- Prefer screenshots using XCB over gnome-screenshot #7143
[nulano, radarhere]
- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151
[radarhere]
- Support reading signed 8-bit TIFF images #7111
[radarhere]
- Added width argument to ImageDraw regular_polygon #7132
[radarhere]
- Support I mode for ImageFilter.BuiltinFilter #7108
[radarhere]
- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112
[radarhere]
- Added unpacker from I;16B to I;16 #7125
[radarhere]
- Support float font sizes #7107
[radarhere]

View File

@ -75,43 +75,42 @@ post-patch:
"""
def test_qtables_leak():
standard_l_qtable = (
# fmt: off
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99,
# fmt: on
)
standard_chrominance_qtable = (
# fmt: off
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
# fmt: on
)
@pytest.mark.parametrize(
"qtables",
(
(standard_l_qtable, standard_chrominance_qtable),
[standard_l_qtable, standard_chrominance_qtable],
),
)
def test_qtables_leak(qtables):
im = hopper("RGB")
standard_l_qtable = [
int(s)
for s in """
16 11 10 16 24 40 51 61
12 12 14 19 26 58 60 55
14 13 16 24 40 57 69 56
14 17 22 29 51 87 80 62
18 22 37 56 68 109 103 77
24 35 55 64 81 104 113 92
49 64 78 87 103 121 120 101
72 92 95 98 112 100 103 99
""".split(
None
)
]
standard_chrominance_qtable = [
int(s)
for s in """
17 18 24 47 99 99 99 99
18 21 26 66 99 99 99 99
24 26 56 99 99 99 99 99
47 66 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
""".split(
None
)
]
qtables = [standard_l_qtable, standard_chrominance_qtable]
for _ in range(iterations):
test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)

BIN
Tests/images/8bit.s.tif Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

View File

@ -461,6 +461,17 @@ def test_apng_save_duration_loop(tmp_path):
assert im.info.get("duration") == 750
def test_apng_save_duplicate_duration(tmp_path):
test_file = str(tmp_path / "temp.png")
frame = Image.new("RGB", (1, 1))
# Test a single duration is correctly combined across duplicate frames
frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
with Image.open(test_file) as im:
assert im.n_frames == 1
assert im.info.get("duration") == 1500
def test_apng_save_disposal(tmp_path):
test_file = str(tmp_path / "temp.png")
size = (128, 64)

View File

@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5
def test_roundtrip_save_all_1(tmp_path):
out = str(tmp_path / "temp.gif")
im = Image.new("1", (1, 1))
im2 = Image.new("1", (1, 1), 1)
im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reloaded:
assert reloaded.getpixel((0, 0)) == 0
reloaded.seek(1)
assert reloaded.getpixel((0, 0)) == 255
@pytest.mark.parametrize(
"path, mode",
(

View File

@ -922,6 +922,19 @@ class TestFileJpeg:
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_repr_jpeg(self):
im = hopper()
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17)
def test_repr_jpeg_error(self):
im = hopper("F")
with pytest.raises(ValueError):
im._repr_jpeg_()
@pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg")

View File

@ -96,10 +96,17 @@ class TestFileTiff:
assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
def test_bigtiff(self):
def test_bigtiff(self, tmp_path):
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
# multistrip support not yet implemented
del im.tag_v2[273]
outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_set_legacy_api(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e:
@ -198,6 +205,12 @@ class TestFileTiff:
with pytest.raises(OSError):
im.save(outfile)
def test_8bit_s(self):
with Image.open("Tests/images/8bit.s.tif") as im:
im.load()
assert im.mode == "L"
assert im.getpixel((50, 50)) == 184
def test_little_endian(self):
with Image.open("Tests/images/16bit.cropped.tif") as im:
assert im.getpixel((0, 0)) == 480

View File

@ -30,15 +30,16 @@ from .helper import assert_image_equal, hopper
ImageFilter.UnsharpMask(10),
),
)
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity(filter_to_apply, mode):
im = hopper(mode)
out = im.filter(filter_to_apply)
assert out.mode == im.mode
assert out.size == im.size
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
out = im.filter(filter_to_apply)
assert out.mode == im.mode
assert out.size == im.size
@pytest.mark.parametrize("mode", ("L", "RGB", "CMYK"))
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity_error(mode):
with pytest.raises(TypeError):
im = hopper(mode)
@ -130,10 +131,12 @@ def test_kernel_not_enough_coefficients():
ImageFilter.Kernel((3, 3), (0, 0))
@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_3x3(mode):
with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss.bmp") as reference:
reference_name = "hopper_emboss"
reference_name += "_I.png" if mode == "I" else ".bmp"
with Image.open("Tests/images/" + reference_name) as reference:
kernel = ImageFilter.Kernel(
(3, 3),
# fmt: off
@ -146,16 +149,20 @@ def test_consistency_3x3(mode):
source = source.split() * 2
reference = reference.split() * 2
assert_image_equal(
Image.merge(mode, source[: len(mode)]).filter(kernel),
Image.merge(mode, reference[: len(mode)]),
)
if mode == "I":
source = source[0].convert(mode)
else:
source = Image.merge(mode, source[: len(mode)])
reference = Image.merge(mode, reference[: len(mode)])
assert_image_equal(source.filter(kernel), reference)
@pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK"))
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_5x5(mode):
with Image.open("Tests/images/hopper.bmp") as source:
with Image.open("Tests/images/hopper_emboss_more.bmp") as reference:
reference_name = "hopper_emboss_more"
reference_name += "_I.png" if mode == "I" else ".bmp"
with Image.open("Tests/images/" + reference_name) as reference:
kernel = ImageFilter.Kernel(
(5, 5),
# fmt: off
@ -170,10 +177,12 @@ def test_consistency_5x5(mode):
source = source.split() * 2
reference = reference.split() * 2
assert_image_equal(
Image.merge(mode, source[: len(mode)]).filter(kernel),
Image.merge(mode, reference[: len(mode)]),
)
if mode == "I":
source = source[0].convert(mode)
else:
source = Image.merge(mode, source[: len(mode)])
reference = Image.merge(mode, reference[: len(mode)])
assert_image_equal(source.filter(kernel), reference)
def test_invalid_box_blur_filter():

View File

@ -32,6 +32,14 @@ def test_putpalette():
with pytest.raises(ValueError):
palette("YCbCr")
with Image.open("Tests/images/hopper_gray.jpg") as im:
assert im.mode == "L"
im.putpalette(list(range(256)) * 3)
with Image.open("Tests/images/la.tga") as im:
assert im.mode == "LA"
im.putpalette(list(range(256)) * 3)
def test_imagepalette():
im = hopper("P")

View File

@ -27,15 +27,21 @@ X1 = int(X0 * 3)
Y0 = int(H / 4)
Y1 = int(X0 * 3)
# Two kinds of bounding box
BBOX1 = [(X0, Y0), (X1, Y1)]
BBOX2 = [X0, Y0, X1, Y1]
# Bounding boxes
BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1])
# Two kinds of coordinate sequences
POINTS1 = [(10, 10), (20, 40), (30, 30)]
POINTS2 = [10, 10, 20, 40, 30, 30]
# Coordinate sequences
POINTS = (
((10, 10), (20, 40), (30, 30)),
[(10, 10), (20, 40), (30, 30)],
(10, 10, 20, 40, 30, 30),
[10, 10, 20, 40, 30, 30],
)
KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]
KITE_POINTS = (
((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
[(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
)
def test_sanity():
@ -63,7 +69,7 @@ def test_mode_mismatch():
ImageDraw.ImageDraw(im, mode="L")
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox, start, end):
# Arrange
@ -77,7 +83,8 @@ def test_arc(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
def test_arc_end_le_start():
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_end_le_start(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@ -85,13 +92,14 @@ def test_arc_end_le_start():
end = 0
# Act
draw.arc(BBOX1, start=start, end=end)
draw.arc(bbox, start=start, end=end)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png")
def test_arc_no_loops():
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_no_loops(bbox):
# No need to go in loops
# Arrange
im = Image.new("RGB", (W, H))
@ -100,57 +108,61 @@ def test_arc_no_loops():
end = 370
# Act
draw.arc(BBOX1, start=start, end=end)
draw.arc(bbox, start=start, end=end)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1)
def test_arc_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.arc(BBOX1, 10, 260, width=5)
draw.arc(bbox, 10, 260, width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1)
def test_arc_width_pieslice_large():
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_pieslice_large(bbox):
# Tests an arc with a large enough width that it is a pieslice
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.arc(BBOX1, 10, 260, fill="yellow", width=100)
draw.arc(bbox, 10, 260, fill="yellow", width=100)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1)
def test_arc_width_fill():
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.arc(BBOX1, 10, 260, fill="yellow", width=5)
draw.arc(bbox, 10, 260, fill="yellow", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1)
def test_arc_width_non_whole_angle():
@pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_non_whole_angle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png"
# Act
draw.arc(BBOX1, 10, 259.5, width=5)
draw.arc(bbox, 10, 259.5, width=5)
# Assert
assert_image_similar_tofile(im, expected, 1)
@ -184,7 +196,7 @@ def test_bitmap():
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("bbox", BBOX)
def test_chord(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
@ -198,37 +210,40 @@ def test_chord(mode, bbox):
assert_image_similar_tofile(im, expected, 1)
def test_chord_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_chord_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.chord(BBOX1, 10, 260, outline="yellow", width=5)
draw.chord(bbox, 10, 260, outline="yellow", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1)
def test_chord_width_fill():
@pytest.mark.parametrize("bbox", BBOX)
def test_chord_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5)
draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1)
def test_chord_zero_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_chord_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0)
draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png")
@ -247,7 +262,7 @@ def test_chord_too_fat():
@pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
@ -261,13 +276,14 @@ def test_ellipse(mode, bbox):
assert_image_similar_tofile(im, expected, 1)
def test_ellipse_translucent():
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_translucent(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
# Act
draw.ellipse(BBOX1, fill=(0, 255, 0, 127))
draw.ellipse(bbox, fill=(0, 255, 0, 127))
# Assert
expected = "Tests/images/imagedraw_ellipse_translucent.png"
@ -297,13 +313,14 @@ def test_ellipse_symmetric():
assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT))
def test_ellipse_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.ellipse(BBOX1, outline="blue", width=5)
draw.ellipse(bbox, outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1)
@ -321,25 +338,27 @@ def test_ellipse_width_large():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1)
def test_ellipse_width_fill():
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.ellipse(BBOX1, fill="green", outline="blue", width=5)
draw.ellipse(bbox, fill="green", outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1)
def test_ellipse_zero_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.ellipse(BBOX1, fill="green", outline="blue", width=0)
draw.ellipse(bbox, fill="green", outline="blue", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png")
@ -386,7 +405,7 @@ def test_ellipse_various_sizes_filled():
)
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
@pytest.mark.parametrize("points", POINTS)
def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
@ -458,7 +477,7 @@ def test_transform():
assert_image_equal(im, expected)
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox, start, end):
# Arrange
@ -472,38 +491,41 @@ def test_pieslice(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
def test_pieslice_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.pieslice(BBOX1, 10, 260, outline="blue", width=5)
draw.pieslice(bbox, 10, 260, outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1)
def test_pieslice_width_fill():
@pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_pieslice_width_fill.png"
# Act
draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5)
draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5)
# Assert
assert_image_similar_tofile(im, expected, 1)
def test_pieslice_zero_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0)
draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png")
@ -551,7 +573,7 @@ def test_pieslice_no_spikes():
assert_image_equal(im, im_pre_erase)
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
@pytest.mark.parametrize("points", POINTS)
def test_point(points):
# Arrange
im = Image.new("RGB", (W, H))
@ -564,7 +586,7 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
@pytest.mark.parametrize("points", POINTS)
def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
@ -578,7 +600,8 @@ def test_polygon(points):
@pytest.mark.parametrize("mode", ("RGB", "L"))
def test_polygon_kite(mode):
@pytest.mark.parametrize("kite_points", KITE_POINTS)
def test_polygon_kite(mode, kite_points):
# Test drawing lines of different gradients (dx>dy, dy>dx) and
# vertical (dx==0) and horizontal (dy==0) lines
# Arrange
@ -587,7 +610,7 @@ def test_polygon_kite(mode):
expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png"
# Act
draw.polygon(KITE_POINTS, fill="blue", outline="yellow")
draw.polygon(kite_points, fill="blue", outline="yellow")
# Assert
assert_image_equal_tofile(im, expected)
@ -634,7 +657,7 @@ def test_polygon_translucent():
assert_image_equal_tofile(im, expected)
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
@ -661,63 +684,68 @@ def test_big_rectangle():
assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1)
def test_rectangle_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_rectangle_width.png"
# Act
draw.rectangle(BBOX1, outline="green", width=5)
draw.rectangle(bbox, outline="green", width=5)
# Assert
assert_image_equal_tofile(im, expected)
def test_rectangle_width_fill():
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_width_fill(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_rectangle_width_fill.png"
# Act
draw.rectangle(BBOX1, fill="blue", outline="green", width=5)
draw.rectangle(bbox, fill="blue", outline="green", width=5)
# Assert
assert_image_equal_tofile(im, expected)
def test_rectangle_zero_width():
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_zero_width(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.rectangle(BBOX1, fill="blue", outline="green", width=0)
draw.rectangle(bbox, fill="blue", outline="green", width=0)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png")
def test_rectangle_I16():
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_I16(bbox):
# Arrange
im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.rectangle(BBOX1, fill="black", outline="green")
draw.rectangle(bbox, fill="black", outline="green")
# Assert
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
def test_rectangle_translucent_outline():
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_translucent_outline(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
# Act
draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5)
draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5)
# Assert
assert_image_equal_tofile(
@ -794,13 +822,14 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type):
)
def test_rounded_rectangle_zero_radius():
@pytest.mark.parametrize("bbox", BBOX)
def test_rounded_rectangle_zero_radius(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
# Act
draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5)
draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png")
@ -810,7 +839,9 @@ def test_rounded_rectangle_zero_radius():
"xy, suffix",
[
((20, 10, 80, 90), "x"),
((20, 10, 81, 90), "x_odd"),
((10, 20, 90, 80), "y"),
((10, 20, 90, 81), "y_odd"),
((20, 20, 80, 80), "both"),
],
)
@ -830,14 +861,15 @@ def test_rounded_rectangle_translucent(xy, suffix):
)
def test_floodfill():
@pytest.mark.parametrize("bbox", BBOX)
def test_floodfill(bbox):
red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
draw.rectangle(BBOX2, outline="yellow", fill="green")
draw.rectangle(bbox, outline="yellow", fill="green")
centre_point = (int(W / 2), int(H / 2))
# Act
@ -862,13 +894,14 @@ def test_floodfill():
assert_image_equal(im, Image.new("RGB", (1, 1), red))
def test_floodfill_border():
@pytest.mark.parametrize("bbox", BBOX)
def test_floodfill_border(bbox):
# floodfill() is experimental
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.rectangle(BBOX2, outline="yellow", fill="green")
draw.rectangle(bbox, outline="yellow", fill="green")
centre_point = (int(W / 2), int(H / 2))
# Act
@ -883,13 +916,14 @@ def test_floodfill_border():
assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png")
def test_floodfill_thresh():
@pytest.mark.parametrize("bbox", BBOX)
def test_floodfill_thresh(bbox):
# floodfill() is experimental
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.rectangle(BBOX2, outline="darkgreen", fill="green")
draw.rectangle(bbox, outline="darkgreen", fill="green")
centre_point = (int(W / 2), int(H / 2))
# Act
@ -1309,7 +1343,8 @@ def test_setting_default_font():
assert isinstance(draw.getfont(), ImageFont.ImageFont)
def test_same_color_outline():
@pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox):
# Prepare shape
x0, y0 = 5, 5
x1, y1 = 5, 50
@ -1325,12 +1360,12 @@ def test_same_color_outline():
for mode in ["RGB", "L"]:
for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]:
for operation, args in {
"chord": [BBOX1, 0, 180],
"ellipse": [BBOX1],
"chord": [bbox, 0, 180],
"ellipse": [bbox],
"shape": [s],
"pieslice": [BBOX1, -90, 45],
"pieslice": [bbox, -90, 45],
"polygon": [[(18, 30), (85, 30), (60, 72)]],
"rectangle": [BBOX1],
"rectangle": [bbox],
}.items():
# Arrange
im = Image.new(mode, (W, H))
@ -1347,20 +1382,20 @@ def test_same_color_outline():
@pytest.mark.parametrize(
"n_sides, rotation, polygon_name",
[(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")],
"n_sides, polygon_name, args",
[
(4, "square", {}),
(8, "regular_octagon", {}),
(4, "square_rotate_45", {"rotation": 45}),
(3, "triangle_width", {"width": 5, "outline": "yellow"}),
],
)
def test_draw_regular_polygon(n_sides, rotation, polygon_name):
def test_draw_regular_polygon(n_sides, polygon_name, args):
im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0))
filename_base = f"Tests/images/imagedraw_{polygon_name}"
filename = (
f"{filename_base}.png"
if rotation == 0
else f"{filename_base}_rotate_{rotation}.png"
)
filename = f"Tests/images/imagedraw_{polygon_name}.png"
draw = ImageDraw.Draw(im)
bounding_circle = ((W // 2, H // 2), 25)
draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red")
draw.regular_polygon(bounding_circle, n_sides, fill="red", **args)
assert_image_equal_tofile(im, filename)

View File

@ -27,15 +27,16 @@ X1 = int(X0 * 3)
Y0 = int(H / 4)
Y1 = int(X0 * 3)
# Two kinds of bounding box
BBOX1 = [(X0, Y0), (X1, Y1)]
BBOX2 = [X0, Y0, X1, Y1]
# Bounding boxes
BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1])
# Two kinds of coordinate sequences
POINTS1 = [(10, 10), (20, 40), (30, 30)]
POINTS2 = [10, 10, 20, 40, 30, 30]
KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]
# Coordinate sequences
POINTS = (
((10, 10), (20, 40), (30, 30)),
[(10, 10), (20, 40), (30, 30)],
(10, 10, 20, 40, 30, 30),
[10, 10, 20, 40, 30, 30],
)
FONT_PATH = "Tests/fonts/FreeMono.ttf"
@ -52,7 +53,7 @@ def test_sanity():
draw.line(list(range(10)), pen)
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(bbox):
# Arrange
im = Image.new("RGB", (W, H))
@ -80,7 +81,7 @@ def test_ellipse_edge():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1)
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
@pytest.mark.parametrize("points", POINTS)
def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
@ -94,7 +95,8 @@ def test_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
def test_line_pen_as_brush():
@pytest.mark.parametrize("points", POINTS)
def test_line_pen_as_brush(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@ -103,13 +105,13 @@ def test_line_pen_as_brush():
# Act
# Pass in the pen as the brush parameter
draw.line(POINTS1, pen, brush)
draw.line(points, pen, brush)
# Assert
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
@pytest.mark.parametrize("points", (POINTS1, POINTS2))
@pytest.mark.parametrize("points", POINTS)
def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
@ -124,7 +126,7 @@ def test_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
@pytest.mark.parametrize("bbox", BBOX)
def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))

View File

@ -463,6 +463,11 @@ def test_default_font():
assert_image_equal_tofile(im, "Tests/images/default_font.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
def test_getbbox(font, mode):
assert (0, 4, 12, 16) == font.getbbox("A", mode)
def test_getbbox_empty(font):
# issue #2614, should not crash.
assert (0, 0, 0, 0) == font.getbbox("")

View File

@ -98,3 +98,18 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
im = ImageGrab.grabclipboard()
assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.skipif(
(
sys.platform != "linux"
or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy"))
),
reason="Linux with wl-clipboard only",
)
@pytest.mark.parametrize("ext", ("gif", "png", "ico"))
def test_grabclipboard_wl_clipboard(self, ext):
image_path = "Tests/images/hopper." + ext
with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard()
assert_image_equal_tofile(im, image_path)

View File

@ -28,7 +28,7 @@ def test_path():
(6.0, 7.0),
(8.0, 9.0),
]
assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
assert p.getbbox() == (0.0, 1.0, 8.0, 9.0)
@ -38,48 +38,65 @@ def test_path():
p.transform((1, 0, 1, 0, 1, 1))
assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)]
# alternative constructors
p = ImagePath.Path([0, 1])
assert list(p) == [(0.0, 1.0)]
p = ImagePath.Path([0.0, 1.0])
assert list(p) == [(0.0, 1.0)]
p = ImagePath.Path([0, 1])
assert list(p) == [(0.0, 1.0)]
p = ImagePath.Path([(0, 1)])
assert list(p) == [(0.0, 1.0)]
p = ImagePath.Path(p)
assert list(p) == [(0.0, 1.0)]
p = ImagePath.Path(p.tolist(0))
assert list(p) == [(0.0, 1.0)]
p = ImagePath.Path(p.tolist(1))
assert list(p) == [(0.0, 1.0)]
p = ImagePath.Path(array.array("f", [0, 1]))
assert list(p) == [(0.0, 1.0)]
arr = array.array("f", [0, 1])
p = ImagePath.Path(arr.tobytes())
@pytest.mark.parametrize(
"coords",
(
(0, 1),
[0, 1],
(0.0, 1.0),
[0.0, 1.0],
((0, 1),),
[(0, 1)],
((0.0, 1.0),),
[(0.0, 1.0)],
array.array("f", [0, 1]),
array.array("f", [0, 1]).tobytes(),
ImagePath.Path((0, 1)),
),
)
def test_path_constructors(coords):
# Arrange / Act
p = ImagePath.Path(coords)
# Assert
assert list(p) == [(0.0, 1.0)]
def test_invalid_coords():
# Arrange
coords = ["a", "b"]
# Act / Assert
@pytest.mark.parametrize(
"coords",
(
("a", "b"),
([0, 1],),
[[0, 1]],
([0.0, 1.0],),
[[0.0, 1.0]],
),
)
def test_invalid_path_constructors(coords):
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
# Assert
assert str(e.value) == "incorrect coordinate type"
def test_path_odd_number_of_coordinates():
# Arrange
coords = [0]
# Act / Assert
@pytest.mark.parametrize(
"coords",
(
(0,),
[0],
(0, 1, 2),
[0, 1, 2],
),
)
def test_path_odd_number_of_coordinates(coords):
# Act
with pytest.raises(ValueError) as e:
ImagePath.Path(coords)
# Assert
assert str(e.value) == "wrong number of coordinates"

View File

@ -757,6 +757,7 @@ class TestLibUnpack:
def test_I16(self):
self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605)
self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506)
self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506)
self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605)
self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040)

View File

@ -1,9 +1,9 @@
# Documentation: https://docs.codecov.io/docs/codecov-yaml
# Documentation: https://docs.codecov.com/docs/codecov-yaml
codecov:
# Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]"
# https://github.com/codecov/support/issues/363
# https://docs.codecov.io/docs/comparing-commits
# https://docs.codecov.com/docs/comparing-commits
allow_coverage_offsets: true
comment: false

View File

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

View File

@ -2,7 +2,7 @@
from livereload.compiler import shell
from livereload.task import Task
Task.add('*.rst', shell('make html'))
Task.add('*/*.rst', shell('make html'))
Task.add('Makefile', shell('make html'))
Task.add('conf.py', shell('make html'))
Task.add("*.rst", shell("make html"))
Task.add("*/*.rst", shell("make html"))
Task.add("Makefile", shell("make html"))
Task.add("conf.py", shell("make html"))

View File

@ -317,6 +317,17 @@ def setup(app):
app.add_css_file("css/dark.css")
linkcheck_allowed_redirects = {
r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501
}
# sphinx.ext.extlinks
# This config is a dictionary of external sites,
# mapping unique short aliases to a base URL and a prefix.

View File

@ -210,7 +210,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde
Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
`PySide6 <https://doc.qt.io/qtforpython-6/>`_ instead.
Image.coerce_e
~~~~~~~~~~~~~~

View File

@ -95,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel
corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5).
Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles
are represented as 4-tuples, with the upper left corner given first. For
example, a rectangle covering all of an 800x600 pixel image is written as (0,
0, 800, 600).
are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given
first.
Palette
-------

View File

@ -181,7 +181,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.1.1**
* Pillow has been tested with libimagequant **2.6-4.2**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.
@ -448,6 +448,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86 |
+----------------------------------+----------------------------+---------------------+
| Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |

View File

@ -243,6 +243,7 @@ Methods
.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None)
Draws a line between the coordinates in the ``xy`` list.
The coordinate pixels are included in the drawn line.
:param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``.
@ -287,7 +288,7 @@ Methods
The polygon outline consists of straight lines between the given
coordinates, plus a straight line between the last and the first
coordinate.
coordinate. The coordinate pixels are included in the drawn polygon.
:param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``.
@ -296,7 +297,7 @@ Methods
:param width: The line width, in pixels.
.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None)
.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1)
Draws a regular polygon inscribed in ``bounding_circle``,
with ``n_sides``, and rotation of ``rotation`` degrees.
@ -311,6 +312,7 @@ Methods
(e.g. ``rotation=90``, applies a 90 degree rotation).
:param fill: Color to use for the fill.
:param outline: Color to use for the outline.
:param width: The line width, in pixels.
.. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1)

View File

@ -15,8 +15,9 @@ or the clipboard to a PIL image memory.
returned as an "RGBA" on macOS, or an "RGB" image otherwise.
If the bounding box is omitted, the entire screen is copied.
On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it
is installed. To capture the default X11 display instead, pass ``xdisplay=""``.
On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return
a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is
installed. To disable this behaviour, pass ``xdisplay=""`` instead.
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
@ -39,9 +40,11 @@ or the clipboard to a PIL image memory.
.. py:function:: grabclipboard()
Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported.
Take a snapshot of the clipboard image, if any.
.. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS)
On Linux, ``wl-paste`` or ``xclip`` is required.
.. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux)
:return: On Windows, an image, a list of filenames,
or None if the clipboard does not contain image data or filenames.
@ -49,3 +52,5 @@ or the clipboard to a PIL image memory.
On Mac, an image,
or None if the clipboard does not contain image data.
On Linux, an image.

View File

@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the
Maps the path through a function.
.. py:method:: PIL.ImagePath.Path.tolist(flat=0)
.. py:method:: PIL.ImagePath.Path.tolist(flat=False)
Converts the path to a Python list [(x, y), …].

View File

@ -117,7 +117,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde
Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
`PySide6 <https://doc.qt.io/qtforpython-6/>`_ instead.
Image.coerce_e
^^^^^^^^^^^^^^
@ -135,10 +135,11 @@ TODO
API Changes
===========
TODO
^^^^
Added line width parameter to ImageDraw regular_polygon
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
An optional line ``width`` parameter has been added to
``ImageDraw.Draw.regular_polygon``.
API Additions
=============
@ -159,7 +160,20 @@ TODO
Other Changes
=============
TODO
^^^^
Support display_jpeg() in IPython
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now
also be used to display images in IPython::
from PIL import Image
from IPython.display import display_jpeg
im = Image.new("RGB", (100, 100), (255, 0, 0))
display_jpeg(im)
Support reading signed 8-bit TIFF images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TIFF images with signed integer data, 8 bits per sample and a photometric
interpretaton of BlackIsZero can now be read.

View File

@ -15,7 +15,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde
Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
`PySide6 <https://doc.qt.io/qtforpython-6/>`_ instead.
FreeTypeFont.getmask2 fill parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -10,6 +10,7 @@
import os
import re
import shutil
import struct
import subprocess
import sys
@ -150,6 +151,7 @@ def _dbg(s, tp=None):
def _find_library_dirs_ldconfig():
# Based on ctypes.util from Python 2
ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig"
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32"
@ -166,14 +168,14 @@ def _find_library_dirs_ldconfig():
# Assuming GLIBC's ldconfig (with option -p)
# Alpine Linux uses musl that can't print cache
args = ["ldconfig", "-p"]
args = [ldconfig, "-p"]
expr = rf".*\({abi_type}.*\) => (.*)"
env = dict(os.environ)
env["LC_ALL"] = "C"
env["LANG"] = "C"
elif sys.platform.startswith("freebsd"):
args = ["ldconfig", "-r"]
args = [ldconfig, "-r"]
expr = r".* => (.*)"
env = {}

View File

@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
if gs_windows_binary is not None:
if not gs_windows_binary:
try:
os.unlink(outfile)
if infile_temp:
os.unlink(infile_temp)
except OSError:
pass
msg = "Unable to locate Ghostscript on paths"
raise OSError(msg)
command[0] = gs_windows_binary
@ -354,7 +361,6 @@ class EpsImageFile(ImageFile.ImageFile):
check_required_header_comments()
if not self._size:
self._size = 1, 1 # errors if this isn't set. why (1,1)?
msg = "cannot determine EPS bounding box"
raise OSError(msg)

View File

@ -879,7 +879,7 @@ def _get_palette_bytes(im):
:param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header
"""
return im.palette.palette
return im.palette.palette if im.palette else b""
def _get_background(im, info_background):

View File

@ -22,11 +22,11 @@ import os
import struct
import sys
from PIL import Image, ImageFile, PngImagePlugin, features
from . import Image, ImageFile, PngImagePlugin, features
enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k:
from PIL import Jpeg2KImagePlugin
from . import Jpeg2KImagePlugin
MAGIC = b"icns"
HEADERSIZE = 8

View File

@ -633,19 +633,34 @@ class Image:
)
)
def _repr_png_(self):
"""iPython display hook support
def _repr_image(self, image_format):
"""Helper function for iPython display hook.
:returns: png version of the image as bytes
:param image_format: Image format.
:returns: image as bytes, saved into the given format.
"""
b = io.BytesIO()
try:
self.save(b, "PNG")
self.save(b, image_format)
except Exception as e:
msg = "Could not save to PNG for display"
msg = f"Could not save to {image_format} for display"
raise ValueError(msg) from e
return b.getvalue()
def _repr_png_(self):
"""iPython display hook support for PNG format.
:returns: PNG version of the image as bytes
"""
return self._repr_image("PNG")
def _repr_jpeg_(self):
"""iPython display hook support for JPEG format.
:returns: JPEG version of the image as bytes
"""
return self._repr_image("JPEG")
@property
def __array_interface__(self):
# numpy array interface support
@ -1108,7 +1123,6 @@ class Image:
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG`
(default).
:returns: A new image
"""
self.load()
@ -1240,7 +1254,7 @@ class Image:
if ymargin is None:
ymargin = xmargin
self.load()
return self._new(self.im.expand(xmargin, ymargin, 0))
return self._new(self.im.expand(xmargin, ymargin))
def filter(self, filter):
"""

View File

@ -18,10 +18,10 @@
import sys
from enum import IntEnum
from PIL import Image
from . import Image
try:
from PIL import _imagingcms
from . import _imagingcms
except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing
# anything in core.
@ -271,7 +271,7 @@ def get_display_profile(handle=None):
if sys.platform != "win32":
return None
from PIL import ImageWin
from . import ImageWin
if isinstance(handle, ImageWin.HDC):
profile = core.get_display_profile_win32(handle, 1)

View File

@ -279,11 +279,11 @@ class ImageDraw:
self.im.paste(im.im, (0, 0) + im.size, mask.im)
def regular_polygon(
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1
):
"""Draw a regular polygon."""
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
self.polygon(xy, fill, outline)
self.polygon(xy, fill, outline, width)
def rectangle(self, xy, fill=None, outline=None, width=1):
"""Draw a rectangle."""
@ -314,11 +314,11 @@ class ImageDraw:
full_x, full_y = False, False
if all(corners):
full_x = d >= x1 - x0
full_x = d >= x1 - x0 - 1
if full_x:
# The two left and two right corners are joined
d = x1 - x0
full_y = d >= y1 - y0
full_y = d >= y1 - y0 - 1
if full_y:
# The two top and two bottom corners are joined
d = y1 - y0

View File

@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter):
class Kernel(BuiltinFilter):
"""
Create a convolution kernel. The current version only
Create a convolution kernel. The current version only
supports 3x3 and 5x5 integer and floating point kernels.
In the current version, kernels can only be applied to
@ -43,9 +43,10 @@ class Kernel(BuiltinFilter):
:param size: Kernel size, given as (width, height). In the current
version, this must be (3,3) or (5,5).
:param kernel: A sequence containing kernel weights.
:param kernel: A sequence containing kernel weights. The kernel will
be flipped vertically before being applied to the image.
:param scale: Scale factor. If given, the result for each pixel is
divided by this value. The default is the sum of the
divided by this value. The default is the sum of the
kernel weights.
:param offset: Offset. If given, this value is added to the result,
after it has been divided by the scale factor.

View File

@ -26,7 +26,6 @@
#
import base64
import math
import os
import sys
import warnings
@ -226,10 +225,6 @@ class FreeTypeFont:
path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine)
def _multiline_split(self, text):
split_character = "\n" if isinstance(text, str) else b"\n"
return text.split(split_character)
def getname(self):
"""
:return: A tuple of the font family (e.g. Helvetica) and the font style
@ -551,28 +546,23 @@ class FreeTypeFont:
:py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking
"""
size, offset = self.font.getsize(
text, mode, direction, features, language, anchor
)
if start is None:
start = (0, 0)
size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2))
offset = offset[0] - stroke_width, offset[1] - stroke_width
im, size, offset = self.font.render(
text,
Image.core.fill,
mode,
direction,
features,
language,
stroke_width,
anchor,
ink,
start[0],
start[1],
Image.MAX_IMAGE_PIXELS,
)
Image._decompression_bomb_check(size)
im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0)
if min(size):
self.font.render(
text,
im.id,
mode,
direction,
features,
language,
stroke_width,
ink,
start[0],
start[1],
)
return im, offset
def font_variant(

View File

@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution.
#
import io
import os
import shutil
import subprocess
@ -61,7 +62,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
left, top, right, bottom = bbox
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
return im
elif shutil.which("gnome-screenshot"):
try:
if not Image.core.HAVE_XCB:
msg = "Pillow was built without XCB support"
raise OSError(msg)
size, data = Image.core.grabscreen_x11(xdisplay)
except OSError:
if (
xdisplay is None
and sys.platform not in ("darwin", "win32")
and shutil.which("gnome-screenshot")
):
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
subprocess.call(["gnome-screenshot", "-f", filepath])
@ -73,15 +84,13 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
im.close()
return im_cropped
return im
# use xdisplay=None for default display on non-win32/macOS systems
if not Image.core.HAVE_XCB:
msg = "Pillow was built without XCB support"
raise OSError(msg)
size, data = Image.core.grabscreen_x11(xdisplay)
im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
if bbox:
im = im.crop(bbox)
return im
else:
raise
else:
im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
if bbox:
im = im.crop(bbox)
return im
def grabclipboard():
@ -120,8 +129,6 @@ def grabclipboard():
files = data[o:].decode("mbcs").split("\0")
return files[: files.index("")]
if isinstance(data, bytes):
import io
data = io.BytesIO(data)
if fmt == "png":
from . import PngImagePlugin
@ -134,16 +141,29 @@ def grabclipboard():
return None
else:
if shutil.which("wl-paste"):
output = subprocess.check_output(["wl-paste", "-l"]).decode()
mimetypes = output.splitlines()
if "image/png" in mimetypes:
mimetype = "image/png"
elif mimetypes:
mimetype = mimetypes[0]
else:
mimetype = None
args = ["wl-paste"]
if mimetype:
args.extend(["-t", mimetype])
elif shutil.which("xclip"):
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
else:
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
raise NotImplementedError(msg)
fh, filepath = tempfile.mkstemp()
subprocess.call(args, stdout=fh)
os.close(fh)
im = Image.open(filepath)
p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
err = p.stderr
if err:
msg = f"{args[0]} error: {err.strip().decode()}"
raise ChildProcessError(msg)
data = io.BytesIO(p.stdout)
im = Image.open(data)
im.load()
os.unlink(filepath)
return im

View File

@ -17,7 +17,7 @@ import subprocess
import sys
from shlex import quote
from PIL import Image
from . import Image
_viewers = []

View File

@ -457,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile):
if os.path.exists(self.filename):
subprocess.check_call(["djpeg", "-outfile", path, self.filename])
else:
try:
os.unlink(path)
except OSError:
pass
msg = "Invalid Filename"
raise ValueError(msg)

View File

@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend")
):
if isinstance(duration, (list, tuple)):
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
previous["encoderinfo"]["duration"] += encoderinfo.get(
"duration", duration
)
continue
else:
bbox = None
if "duration" not in encoderinfo:
encoderinfo["duration"] = duration
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
# animation control
@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
im_frame = im_frame.crop(bbox)
size = im_frame.size
encoderinfo = frame_data["encoderinfo"]
frame_duration = int(round(encoderinfo.get("duration", duration)))
frame_duration = int(round(encoderinfo["duration"]))
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
# frame control

View File

@ -36,7 +36,7 @@ import os
import struct
import sys
from PIL import Image, ImageFile
from . import Image, ImageFile
def isInt(f):
@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile):
# returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self):
from PIL import ImageTk
from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256)

View File

@ -170,6 +170,8 @@ OPEN_INFO = {
(MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
(II, 1, (1,), 1, (8,), ()): ("L", "L"),
(MM, 1, (1,), 1, (8,), ()): ("L", "L"),
(II, 1, (2,), 1, (8,), ()): ("L", "L"),
(MM, 1, (2,), 1, (8,), ()): ("L", "L"),
(II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
@ -1892,6 +1894,10 @@ class AppendingTiffWriter:
8, # srational
4, # float
8, # double
4, # ifd
2, # unicode
4, # complex
8, # long8
]
# StripOffsets = 273

View File

@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) {
static PyObject *
_expand_image(ImagingObject *self, PyObject *args) {
int x, y;
int mode = 0;
if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) {
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
return PyImagingNew(ImagingExpand(self->image, x, y, mode));
return PyImagingNew(ImagingExpand(self->image, x, y));
}
static PyObject *

View File

@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
return NULL;
}
#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11
PyConfig config;
PyConfig_InitPythonConfig(&config);
if (!PyArg_ParseTupleAndKeywords(
args,
kw,
"etf|nsy#n",
kwlist,
config.filesystem_encoding,
&filename,
&size,
&index,
&encoding,
&font_bytes,
&font_bytes_size,
&layout_engine)) {
PyConfig_Clear(&config);
return NULL;
}
PyConfig_Clear(&config);
#else
if (!PyArg_ParseTupleAndKeywords(
args,
kw,
@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) {
&layout_engine)) {
return NULL;
}
#endif
self = PyObject_New(FontObject, &Font_Type);
if (!self) {
@ -232,9 +254,7 @@ text_layout_raqm(
const char *dir,
PyObject *features,
const char *lang,
GlyphInfo **glyph_info,
int mask,
int color) {
GlyphInfo **glyph_info) {
size_t i = 0, count = 0, start = 0;
raqm_t *rq;
raqm_glyph_t *glyphs = NULL;
@ -471,7 +491,7 @@ text_layout(
#ifdef HAVE_RAQM
if (have_raqm && self->layout_engine == LAYOUT_RAQM) {
count = text_layout_raqm(
string, self, dir, features, lang, glyph_info, mask, color);
string, self, dir, features, lang, glyph_info);
} else
#endif
{
@ -529,73 +549,25 @@ font_getlength(FontObject *self, PyObject *args) {
return PyLong_FromLong(length);
}
static PyObject *
font_getsize(FontObject *self, PyObject *args) {
static int
bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) {
int position; /* pen position along primary axis, in 26.6 precision */
int advanced; /* pen position along primary axis, in pixels */
int px, py; /* position of current glyph, in pixels */
int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */
int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */
int load_flags; /* FreeType load_flags parameter */
int error;
FT_Face face;
FT_Glyph glyph;
FT_BBox bbox; /* glyph bounding box */
GlyphInfo *glyph_info = NULL; /* computed text layout */
size_t i, count; /* glyph_info index and length */
int horizontal_dir; /* is primary axis horizontal? */
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */
const char *mode = NULL;
const char *dir = NULL;
const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None;
PyObject *string;
/* calculate size and bearing for a given string */
if (!PyArg_ParseTuple(
args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
return NULL;
}
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0;
if (anchor == NULL) {
anchor = horizontal_dir ? "la" : "lt";
}
if (strlen(anchor) != 2) {
goto bad_anchor;
}
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) {
return NULL;
}
load_flags = FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
FT_BBox bbox; /* glyph bounding box */
size_t i; /* glyph_info index */
/*
* text bounds are given by:
* - bounding boxes of individual glyphs
* - pen line, i.e. 0 to `advanced` along primary axis
* this means point (0, 0) is part of the text bounding box
*/
face = NULL;
position = x_min = x_max = y_min = y_max = 0;
for (i = 0; i < count; i++) {
face = self->face;
if (horizontal_dir) {
px = PIXEL(position + glyph_info[i].x_offset);
py = PIXEL(glyph_info[i].y_offset);
@ -618,12 +590,14 @@ font_getsize(FontObject *self, PyObject *args) {
error = FT_Load_Glyph(face, glyph_info[i].index, load_flags);
if (error) {
return geterror(error);
geterror(error);
return 1;
}
error = FT_Get_Glyph(face->glyph, &glyph);
if (error) {
return geterror(error);
geterror(error);
return 1;
}
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox);
@ -647,13 +621,15 @@ font_getsize(FontObject *self, PyObject *args) {
FT_Done_Glyph(glyph);
}
if (glyph_info) {
PyMem_Free(glyph_info);
glyph_info = NULL;
if (anchor == NULL) {
anchor = horizontal_dir ? "la" : "lt";
}
if (strlen(anchor) != 2) {
goto bad_anchor;
}
x_anchor = y_anchor = 0;
if (face) {
if (count) {
if (horizontal_dir) {
switch (anchor[0]) {
case 'l': // left
@ -671,15 +647,15 @@ font_getsize(FontObject *self, PyObject *args) {
}
switch (anchor[1]) {
case 'a': // ascender
y_anchor = PIXEL(self->face->size->metrics.ascender);
y_anchor = PIXEL(face->size->metrics.ascender);
break;
case 't': // top
y_anchor = y_max;
break;
case 'm': // middle (ascender + descender) / 2
y_anchor = PIXEL(
(self->face->size->metrics.ascender +
self->face->size->metrics.descender) /
(face->size->metrics.ascender +
face->size->metrics.descender) /
2);
break;
case 's': // horizontal baseline
@ -689,7 +665,7 @@ font_getsize(FontObject *self, PyObject *args) {
y_anchor = y_min;
break;
case 'd': // descender
y_anchor = PIXEL(self->face->size->metrics.descender);
y_anchor = PIXEL(face->size->metrics.descender);
break;
default:
goto bad_anchor;
@ -729,17 +705,74 @@ font_getsize(FontObject *self, PyObject *args) {
}
}
}
return Py_BuildValue(
"(ii)(ii)",
(x_max - x_min),
(y_max - y_min),
(-x_anchor + x_min),
-(-y_anchor + y_max));
*width = x_max - x_min;
*height = y_max - y_min;
*x_offset = -x_anchor + x_min;
*y_offset = -(-y_anchor + y_max);
return 0;
bad_anchor:
PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor);
return NULL;
return 1;
}
static PyObject *
font_getsize(FontObject *self, PyObject *args) {
int width, height, x_offset, y_offset;
int load_flags; /* FreeType load_flags parameter */
int error;
GlyphInfo *glyph_info = NULL; /* computed text layout */
size_t count; /* glyph_info length */
int horizontal_dir; /* is primary axis horizontal? */
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */
const char *mode = NULL;
const char *dir = NULL;
const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None;
PyObject *string;
/* calculate size and bearing for a given string */
if (!PyArg_ParseTuple(
args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) {
return NULL;
}
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
mask = mode && strcmp(mode, "1") == 0;
color = mode && strcmp(mode, "RGBA") == 0;
count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color);
if (PyErr_Occurred()) {
return NULL;
}
load_flags = FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
if (glyph_info) {
PyMem_Free(glyph_info);
glyph_info = NULL;
}
if (error) {
return NULL;
}
return Py_BuildValue(
"(ii)(ii)",
width,
height,
x_offset,
y_offset);
}
static PyObject *
@ -763,6 +796,7 @@ font_render(FontObject *self, PyObject *args) {
unsigned int bitmap_y; /* glyph bitmap y index */
unsigned char *source; /* glyph bitmap source buffer */
unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */
PyObject *image;
Imaging im;
Py_ssize_t id;
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
@ -773,27 +807,34 @@ font_render(FontObject *self, PyObject *args) {
const char *mode = NULL;
const char *dir = NULL;
const char *lang = NULL;
const char *anchor = NULL;
PyObject *features = Py_None;
PyObject *string;
PyObject *fill;
float x_start = 0;
float y_start = 0;
int width, height, x_offset, y_offset;
int horizontal_dir; /* is primary axis horizontal? */
PyObject *max_image_pixels = Py_None;
/* render string into given buffer (the buffer *must* have
the right size, or this will crash) */
if (!PyArg_ParseTuple(
args,
"On|zzOziLff:render",
"OO|zzOzizLffO:render",
&string,
&id,
&fill,
&mode,
&dir,
&features,
&lang,
&stroke_width,
&anchor,
&foreground_ink_long,
&x_start,
&y_start)) {
&y_start,
&max_image_pixels)) {
return NULL;
}
@ -819,8 +860,41 @@ font_render(FontObject *self, PyObject *args) {
if (PyErr_Occurred()) {
return NULL;
}
if (count == 0) {
Py_RETURN_NONE;
load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1;
error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset);
if (error) {
PyMem_Del(glyph_info);
return NULL;
}
width += stroke_width * 2 + ceil(x_start);
height += stroke_width * 2 + ceil(y_start);
if (max_image_pixels != Py_None) {
if (width * height > PyLong_AsLong(max_image_pixels) * 2) {
PyMem_Del(glyph_info);
return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0);
}
}
image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id"));
im = (Imaging)id;
x_offset -= stroke_width;
y_offset -= stroke_width;
if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info);
return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset);
}
if (stroke_width) {
@ -837,15 +911,6 @@ font_render(FontObject *self, PyObject *args) {
0);
}
im = (Imaging)id;
load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
if (color) {
load_flags |= FT_LOAD_COLOR;
}
/*
* calculate x_min and y_max
* must match font_getsize or there may be clipping!
@ -1042,7 +1107,7 @@ font_render(FontObject *self, PyObject *args) {
}
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
Py_RETURN_NONE;
return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset);
glyph_error:
if (stroker != NULL) {

View File

@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL};
if (!OpenClipboard(NULL)) {
PyErr_SetString(PyExc_OSError, "failed to open clipboard");
return NULL;
// Maybe the clipboard is temporarily in use by another process.
// Wait and try again
Sleep(500);
if (!OpenClipboard(NULL)) {
PyErr_SetString(PyExc_OSError, "failed to open clipboard");
return NULL;
}
}
// find best format as set by clipboard owner

View File

@ -37,8 +37,19 @@ clip8(float in) {
return (UINT8)in;
}
static inline INT32
clip32(float in) {
if (in <= 0.0) {
return 0;
}
if (in >= pow(2, 31) - 1) {
return pow(2, 31) - 1;
}
return (INT32)in;
}
Imaging
ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
ImagingExpand(Imaging imIn, int xmargin, int ymargin) {
Imaging imOut;
int x, y;
ImagingSectionCookie cookie;
@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) {
void
ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
#define KERNEL1x3(in0, x, kernel, d) \
(_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \
_i2f((UINT8)in0[x + d]) * (kernel)[2])
(_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \
_i2f(in0[x + d]) * (kernel)[2])
int x = 0, y = 0;
@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
if (im->bands == 1) {
// Add one time for rounding
offset += 0.5;
for (y = 1; y < im->ysize - 1; y++) {
UINT8 *in_1 = (UINT8 *)im->image[y - 1];
UINT8 *in0 = (UINT8 *)im->image[y];
UINT8 *in1 = (UINT8 *)im->image[y + 1];
UINT8 *out = (UINT8 *)imOut->image[y];
if (im->type == IMAGING_TYPE_INT32) {
for (y = 1; y < im->ysize - 1; y++) {
INT32 *in_1 = (INT32 *)im->image[y - 1];
INT32 *in0 = (INT32 *)im->image[y];
INT32 *in1 = (INT32 *)im->image[y + 1];
INT32 *out = (INT32 *)imOut->image[y];
out[0] = in0[0];
for (x = 1; x < im->xsize - 1; x++) {
float ss = offset;
ss += KERNEL1x3(in1, x, &kernel[0], 1);
ss += KERNEL1x3(in0, x, &kernel[3], 1);
ss += KERNEL1x3(in_1, x, &kernel[6], 1);
out[x] = clip8(ss);
out[0] = in0[0];
for (x = 1; x < im->xsize - 1; x++) {
float ss = offset;
ss += KERNEL1x3(in1, x, &kernel[0], 1);
ss += KERNEL1x3(in0, x, &kernel[3], 1);
ss += KERNEL1x3(in_1, x, &kernel[6], 1);
out[x] = clip32(ss);
}
out[x] = in0[x];
}
} else {
for (y = 1; y < im->ysize - 1; y++) {
UINT8 *in_1 = (UINT8 *)im->image[y - 1];
UINT8 *in0 = (UINT8 *)im->image[y];
UINT8 *in1 = (UINT8 *)im->image[y + 1];
UINT8 *out = (UINT8 *)imOut->image[y];
out[0] = in0[0];
for (x = 1; x < im->xsize - 1; x++) {
float ss = offset;
ss += KERNEL1x3(in1, x, &kernel[0], 1);
ss += KERNEL1x3(in0, x, &kernel[3], 1);
ss += KERNEL1x3(in_1, x, &kernel[6], 1);
out[x] = clip8(ss);
}
out[x] = in0[x];
}
out[x] = in0[x];
}
} else {
// Add one time for rounding
@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) {
void
ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
#define KERNEL1x5(in0, x, kernel, d) \
(_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \
_i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \
_i2f((UINT8)in0[x + d]) * (kernel)[3] + \
_i2f((UINT8)in0[x + d + d]) * (kernel)[4])
(_i2f(in0[x - d - d]) * (kernel)[0] + \
_i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \
_i2f(in0[x + d]) * (kernel)[3] + \
_i2f(in0[x + d + d]) * (kernel)[4])
int x = 0, y = 0;
@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) {
if (im->bands == 1) {
// Add one time for rounding
offset += 0.5;
for (y = 2; y < im->ysize - 2; y++) {
UINT8 *in_2 = (UINT8 *)im->image[y - 2];
UINT8 *in_1 = (UINT8 *)im->image[y - 1];
UINT8 *in0 = (UINT8 *)im->image[y];
UINT8 *in1 = (UINT8 *)im->image[y + 1];
UINT8 *in2 = (UINT8 *)im->image[y + 2];
UINT8 *out = (UINT8 *)imOut->image[y];
if (im->type == IMAGING_TYPE_INT32) {
for (y = 2; y < im->ysize - 2; y++) {
INT32 *in_2 = (INT32 *)im->image[y - 2];
INT32 *in_1 = (INT32 *)im->image[y - 1];
INT32 *in0 = (INT32 *)im->image[y];
INT32 *in1 = (INT32 *)im->image[y + 1];
INT32 *in2 = (INT32 *)im->image[y + 2];
INT32 *out = (INT32 *)imOut->image[y];
out[0] = in0[0];
out[1] = in0[1];
for (x = 2; x < im->xsize - 2; x++) {
float ss = offset;
ss += KERNEL1x5(in2, x, &kernel[0], 1);
ss += KERNEL1x5(in1, x, &kernel[5], 1);
ss += KERNEL1x5(in0, x, &kernel[10], 1);
ss += KERNEL1x5(in_1, x, &kernel[15], 1);
ss += KERNEL1x5(in_2, x, &kernel[20], 1);
out[x] = clip8(ss);
out[0] = in0[0];
out[1] = in0[1];
for (x = 2; x < im->xsize - 2; x++) {
float ss = offset;
ss += KERNEL1x5(in2, x, &kernel[0], 1);
ss += KERNEL1x5(in1, x, &kernel[5], 1);
ss += KERNEL1x5(in0, x, &kernel[10], 1);
ss += KERNEL1x5(in_1, x, &kernel[15], 1);
ss += KERNEL1x5(in_2, x, &kernel[20], 1);
out[x] = clip32(ss);
}
out[x + 0] = in0[x + 0];
out[x + 1] = in0[x + 1];
}
} else {
for (y = 2; y < im->ysize - 2; y++) {
UINT8 *in_2 = (UINT8 *)im->image[y - 2];
UINT8 *in_1 = (UINT8 *)im->image[y - 1];
UINT8 *in0 = (UINT8 *)im->image[y];
UINT8 *in1 = (UINT8 *)im->image[y + 1];
UINT8 *in2 = (UINT8 *)im->image[y + 2];
UINT8 *out = (UINT8 *)imOut->image[y];
out[0] = in0[0];
out[1] = in0[1];
for (x = 2; x < im->xsize - 2; x++) {
float ss = offset;
ss += KERNEL1x5(in2, x, &kernel[0], 1);
ss += KERNEL1x5(in1, x, &kernel[5], 1);
ss += KERNEL1x5(in0, x, &kernel[10], 1);
ss += KERNEL1x5(in_1, x, &kernel[15], 1);
ss += KERNEL1x5(in_2, x, &kernel[20], 1);
out[x] = clip8(ss);
}
out[x + 0] = in0[x + 0];
out[x + 1] = in0[x + 1];
}
out[x + 0] = in0[x + 0];
out[x + 1] = in0[x + 1];
}
} else {
// Add one time for rounding
@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o
Imaging imOut;
ImagingSectionCookie cookie;
if (!im || im->type != IMAGING_TYPE_UINT8) {
if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) {
return (Imaging)ImagingError_ModeError();
}

View File

@ -58,12 +58,6 @@
#error Cannot find required 32-bit integer type
#endif
#if SIZEOF_LONG == 8
#define INT64 long
#elif SIZEOF_LONG_LONG == 8
#define INT64 long
#endif
#define INT8 signed char
#define UINT8 unsigned char

View File

@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b);
extern Imaging
ImagingCrop(Imaging im, int x0, int y0, int x1, int y1);
extern Imaging
ImagingExpand(Imaging im, int x, int y, int mode);
ImagingExpand(Imaging im, int x, int y);
extern Imaging
ImagingFill(Imaging im, const void *ink);
extern int

View File

@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
}
if (!context->num_resolutions) {
while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) {
while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) {
params.numresolution -= 1;
}
}

View File

@ -37,8 +37,6 @@
#include "Imaging.h"
#include <string.h>
int ImagingNewCount = 0;
/* --------------------------------------------------------------------
* Standard image object.
*/

View File

@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) {
}
}
static void
unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) {
int i;
for (i = 0; i < pixels; i++) {
out[0] = in[1];
out[1] = in[0];
in += 2;
out += 2;
}
}
static void
unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) {
int i;
for (i = 0; i < pixels; i++) {
@ -1542,10 +1552,12 @@ static struct {
{"P", "P;4L", 4, unpackP4L},
{"P", "P", 8, copy1},
{"P", "P;R", 8, unpackLR},
{"P", "L", 8, copy1},
/* palette w. alpha */
{"PA", "PA", 16, unpackLA},
{"PA", "PA;L", 16, unpackLAL},
{"PA", "LA", 16, unpackLA},
/* true colour */
{"RGB", "RGB", 24, ImagingUnpackRGB},
@ -1764,6 +1776,7 @@ static struct {
{"I;16L", "I;16L", 16, copy2},
{"I;16N", "I;16N", 16, copy2},
{"I;16", "I;16B", 16, unpackI16B_I16},
{"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian.
{"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian.
{"I;16B", "I;16N", 16, unpackI16N_I16B},

View File

@ -152,9 +152,9 @@ deps = {
"libs": [r"*.lib"],
},
"xz": {
"url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download",
"filename": "xz-5.4.2.tar.gz",
"dir": "xz-5.4.2",
"url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download",
"filename": "xz-5.4.3.tar.gz",
"dir": "xz-5.4.3",
"license": "COPYING",
"build": [
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
@ -337,9 +337,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip",
"filename": "harfbuzz-7.2.0.zip",
"dir": "harfbuzz-7.2.0",
"url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip",
"filename": "harfbuzz-7.3.0.zip",
"dir": "harfbuzz-7.3.0",
"license": "COPYING",
"build": [
*cmds_cmake(
@ -352,12 +352,12 @@ deps = {
"libs": [r"*.lib"],
},
"fribidi": {
"url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip",
"filename": "fribidi-1.0.12.zip",
"dir": "fribidi-1.0.12",
"url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip",
"filename": "fribidi-1.0.13.zip",
"dir": "fribidi-1.0.13",
"license": "COPYING",
"build": [
cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"),
cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"),
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
*cmds_cmake("fribidi"),
],