mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-12 08:14:45 +03:00
Merge branch 'main' into closed
This commit is contained in:
commit
8984b630e3
2
.github/workflows/cifuzz.yml
vendored
2
.github/workflows/cifuzz.yml
vendored
|
@ -3,10 +3,12 @@ name: CIFuzz
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/cifuzz.yml"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/cifuzz.yml"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
workflow_dispatch:
|
||||
|
|
52
.github/workflows/docs.yml
vendored
Normal file
52
.github/workflows/docs.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
name: Docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
cache-dependency-path: ".ci/*.sh"
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
.ci/install.sh
|
||||
env:
|
||||
GHA_PYTHON_VERSION: "3.x"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
.ci/build.sh
|
||||
|
||||
- name: Docs
|
||||
run: |
|
||||
make doccheck
|
11
.github/workflows/test-cygwin.yml
vendored
11
.github/workflows/test-cygwin.yml
vendored
|
@ -1,6 +1,15 @@
|
|||
name: Test Cygwin
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
11
.github/workflows/test-docker.yml
vendored
11
.github/workflows/test-docker.yml
vendored
|
@ -1,6 +1,15 @@
|
|||
name: Test Docker
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
11
.github/workflows/test-mingw.yml
vendored
11
.github/workflows/test-mingw.yml
vendored
|
@ -1,6 +1,15 @@
|
|||
name: Test MinGW
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
2
.github/workflows/test-valgrind.yml
vendored
|
@ -5,10 +5,12 @@ name: Test Valgrind
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/test-valgrind.yml"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/test-valgrind.yml"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
workflow_dispatch:
|
||||
|
|
11
.github/workflows/test-windows.yml
vendored
11
.github/workflows/test-windows.yml
vendored
|
@ -1,6 +1,15 @@
|
|||
name: Test Windows
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
|
@ -1,6 +1,15 @@
|
|||
name: Test
|
||||
|
||||
on: [push, pull_request, workflow_dispatch]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
@ -96,11 +105,6 @@ jobs:
|
|||
name: errors
|
||||
path: Tests/errors
|
||||
|
||||
- name: Docs
|
||||
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11
|
||||
run: |
|
||||
make doccheck
|
||||
|
||||
- name: After success
|
||||
run: |
|
||||
.ci/after_success.sh
|
||||
|
|
|
@ -177,13 +177,14 @@ class TestEnvVars:
|
|||
Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"})
|
||||
assert Image.core.get_block_size() == 2 * 1024 * 1024
|
||||
|
||||
def test_warnings(self):
|
||||
pytest.warns(
|
||||
UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"}
|
||||
)
|
||||
pytest.warns(
|
||||
UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"}
|
||||
)
|
||||
pytest.warns(
|
||||
UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"}
|
||||
@pytest.mark.parametrize(
|
||||
"var",
|
||||
(
|
||||
{"PILLOW_ALIGNMENT": "15"},
|
||||
{"PILLOW_BLOCK_SIZE": "1024"},
|
||||
{"PILLOW_BLOCKS_MAX": "wat"},
|
||||
),
|
||||
)
|
||||
def test_warnings(self, var):
|
||||
with pytest.warns(UserWarning):
|
||||
Image._apply_env_variables(var)
|
||||
|
|
|
@ -36,12 +36,10 @@ class TestDecompressionBomb:
|
|||
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
|
||||
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
|
||||
|
||||
def open():
|
||||
with pytest.warns(Image.DecompressionBombWarning):
|
||||
with Image.open(TEST_FILE):
|
||||
pass
|
||||
|
||||
pytest.warns(Image.DecompressionBombWarning, open)
|
||||
|
||||
def test_exception(self):
|
||||
# Set limit to trigger exception on the test file
|
||||
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
|
||||
|
@ -87,7 +85,8 @@ class TestDecompressionCrop:
|
|||
# same decompression bomb warnings on them.
|
||||
with hopper() as src:
|
||||
box = (0, 0, src.width * 2, src.height * 2)
|
||||
pytest.warns(Image.DecompressionBombWarning, src.crop, box)
|
||||
with pytest.warns(Image.DecompressionBombWarning):
|
||||
src.crop(box)
|
||||
|
||||
def test_crop_decompression_checks(self):
|
||||
im = Image.new("RGB", (100, 100))
|
||||
|
@ -95,7 +94,8 @@ class TestDecompressionCrop:
|
|||
for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)):
|
||||
assert im.crop(value).size == (9, 9)
|
||||
|
||||
pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99))
|
||||
with pytest.warns(Image.DecompressionBombWarning):
|
||||
im.crop((-160, -160, 99, 99))
|
||||
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
im.crop((-99909, -99990, 99999, 99999))
|
||||
|
|
|
@ -263,13 +263,11 @@ def test_apng_chunk_errors():
|
|||
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
||||
assert not im.is_animated
|
||||
|
||||
def open():
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
||||
im.load()
|
||||
assert not im.is_animated
|
||||
|
||||
pytest.warns(UserWarning, open)
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
|
||||
assert not im.is_animated
|
||||
|
||||
|
@ -287,21 +285,17 @@ def test_apng_chunk_errors():
|
|||
|
||||
|
||||
def test_apng_syntax_errors():
|
||||
def open_frames_zero():
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
||||
assert not im.is_animated
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
pytest.warns(UserWarning, open_frames_zero)
|
||||
|
||||
def open_frames_zero_default():
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
||||
assert not im.is_animated
|
||||
im.load()
|
||||
|
||||
pytest.warns(UserWarning, open_frames_zero_default)
|
||||
|
||||
# we can handle this case gracefully
|
||||
exception = None
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
||||
|
@ -316,13 +310,11 @@ def test_apng_syntax_errors():
|
|||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
def open():
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
||||
assert not im.is_animated
|
||||
im.load()
|
||||
|
||||
pytest.warns(UserWarning, open)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
|
|
|
@ -28,7 +28,8 @@ def test_unclosed_file():
|
|||
im = Image.open(TEST_FILE)
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
|
|
|
@ -36,7 +36,8 @@ def test_unclosed_file():
|
|||
im = Image.open(static_test_file)
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
|
|
|
@ -36,7 +36,8 @@ def test_unclosed_file():
|
|||
im = Image.open(TEST_GIF)
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
|
@ -1087,7 +1088,8 @@ def test_rgb_transparency(tmp_path):
|
|||
im = Image.new("RGB", (1, 1))
|
||||
im.info["transparency"] = b""
|
||||
ims = [Image.new("RGB", (1, 1))]
|
||||
pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims)
|
||||
with pytest.warns(UserWarning):
|
||||
im.save(out, save_all=True, append_images=ims)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert "transparency" not in reloaded.info
|
||||
|
|
|
@ -212,12 +212,10 @@ def test_save_append_images(tmp_path):
|
|||
def test_unexpected_size():
|
||||
# This image has been manually hexedited to state that it is 16x32
|
||||
# while the image within is still 16x16
|
||||
def open():
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/hopper_unexpected.ico") as im:
|
||||
assert im.size == (16, 16)
|
||||
|
||||
pytest.warns(UserWarning, open)
|
||||
|
||||
|
||||
def test_draw_reloaded(tmp_path):
|
||||
with Image.open(TEST_ICO_FILE) as im:
|
||||
|
|
|
@ -32,7 +32,8 @@ def test_unclosed_file():
|
|||
im = Image.open(TEST_IM)
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
|
|
|
@ -1065,3 +1065,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
out = str(tmp_path / "temp.tif")
|
||||
with pytest.raises(SystemError):
|
||||
im.save(out, compression=compression)
|
||||
|
||||
def test_save_many_compressed(self, tmp_path):
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.tif")
|
||||
for _ in range(10000):
|
||||
im.save(out, compression="jpeg")
|
||||
|
|
|
@ -42,7 +42,8 @@ def test_unclosed_file():
|
|||
im = Image.open(test_files[0])
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
|
|
|
@ -27,7 +27,8 @@ def test_unclosed_file():
|
|||
im = Image.open(test_file)
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
|
|
|
@ -25,7 +25,8 @@ def test_unclosed_file():
|
|||
im = Image.open(TEST_FILE)
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
|
||||
def test_closed_file():
|
||||
|
|
|
@ -29,11 +29,9 @@ def test_sanity(codec, test_path, format):
|
|||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file():
|
||||
def open():
|
||||
with pytest.warns(ResourceWarning):
|
||||
TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg")
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
|
||||
|
||||
def test_close():
|
||||
with warnings.catch_warnings():
|
||||
|
|
|
@ -163,7 +163,9 @@ def test_save_id_section(tmp_path):
|
|||
|
||||
# Save with custom id section greater than 255 characters
|
||||
id_section = b"Test content" * 25
|
||||
pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section))
|
||||
with pytest.warns(UserWarning):
|
||||
im.save(out, id_section=id_section)
|
||||
|
||||
with Image.open(out) as test_im:
|
||||
assert test_im.info["id_section"] == id_section[:255]
|
||||
|
||||
|
|
|
@ -61,7 +61,8 @@ class TestFileTiff:
|
|||
im = Image.open("Tests/images/multipage.tiff")
|
||||
im.load()
|
||||
|
||||
pytest.warns(ResourceWarning, open)
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
|
||||
def test_closed_file(self):
|
||||
with warnings.catch_warnings():
|
||||
|
@ -231,7 +232,8 @@ class TestFileTiff:
|
|||
def test_bad_exif(self):
|
||||
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
|
||||
# Should not raise struct.error.
|
||||
pytest.warns(UserWarning, i._getexif)
|
||||
with pytest.warns(UserWarning):
|
||||
i._getexif()
|
||||
|
||||
def test_save_rgba(self, tmp_path):
|
||||
im = hopper("RGBA")
|
||||
|
|
|
@ -252,7 +252,8 @@ def test_empty_metadata():
|
|||
head = f.read(8)
|
||||
info = TiffImagePlugin.ImageFileDirectory(head)
|
||||
# Should not raise struct.error.
|
||||
pytest.warns(UserWarning, info.load, f)
|
||||
with pytest.warns(UserWarning):
|
||||
info.load(f)
|
||||
|
||||
|
||||
def test_iccprofile(tmp_path):
|
||||
|
@ -418,11 +419,12 @@ def test_too_many_entries():
|
|||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
|
||||
# 277: ("SamplesPerPixel", SHORT, 1),
|
||||
ifd._tagdata[277] = struct.pack("hh", 4, 4)
|
||||
ifd._tagdata[277] = struct.pack("<hh", 4, 4)
|
||||
ifd.tagtype[277] = TiffTags.SHORT
|
||||
|
||||
# Should not raise ValueError.
|
||||
pytest.warns(UserWarning, lambda: ifd[277])
|
||||
with pytest.warns(UserWarning):
|
||||
assert ifd[277] == 4
|
||||
|
||||
|
||||
def test_tag_group_data():
|
||||
|
|
|
@ -29,7 +29,10 @@ class TestUnsupportedWebp:
|
|||
WebPImagePlugin.SUPPORTED = False
|
||||
|
||||
file_path = "Tests/images/hopper.webp"
|
||||
pytest.warns(UserWarning, lambda: pytest.raises(OSError, Image.open, file_path))
|
||||
with pytest.warns(UserWarning):
|
||||
with pytest.raises(OSError):
|
||||
with Image.open(file_path):
|
||||
pass
|
||||
|
||||
if HAVE_WEBP:
|
||||
WebPImagePlugin.SUPPORTED = True
|
||||
|
|
|
@ -132,22 +132,26 @@ class TestImageGetPixel(AccessTest):
|
|||
return 1
|
||||
return tuple(range(1, bands + 1))
|
||||
|
||||
def check(self, mode, c=None):
|
||||
if not c:
|
||||
c = self.color(mode)
|
||||
def check(self, mode, expected_color=None):
|
||||
if not expected_color:
|
||||
expected_color = self.color(mode)
|
||||
|
||||
# check putpixel
|
||||
im = Image.new(mode, (1, 1), None)
|
||||
im.putpixel((0, 0), c)
|
||||
assert (
|
||||
im.getpixel((0, 0)) == c
|
||||
), f"put/getpixel roundtrip failed for mode {mode}, color {c}"
|
||||
im.putpixel((0, 0), expected_color)
|
||||
actual_color = im.getpixel((0, 0))
|
||||
assert actual_color == expected_color, (
|
||||
f"put/getpixel roundtrip failed for mode {mode}, "
|
||||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# check putpixel negative index
|
||||
im.putpixel((-1, -1), c)
|
||||
assert (
|
||||
im.getpixel((-1, -1)) == c
|
||||
), f"put/getpixel roundtrip negative index failed for mode {mode}, color {c}"
|
||||
im.putpixel((-1, -1), expected_color)
|
||||
actual_color = im.getpixel((-1, -1))
|
||||
assert actual_color == expected_color, (
|
||||
f"put/getpixel roundtrip negative index failed for mode {mode}, "
|
||||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0), None)
|
||||
|
@ -155,27 +159,32 @@ class TestImageGetPixel(AccessTest):
|
|||
|
||||
error = ValueError if self._need_cffi_access else IndexError
|
||||
with pytest.raises(error):
|
||||
im.putpixel((0, 0), c)
|
||||
im.putpixel((0, 0), expected_color)
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
with pytest.raises(error):
|
||||
im.putpixel((-1, -1), c)
|
||||
im.putpixel((-1, -1), expected_color)
|
||||
with pytest.raises(error):
|
||||
im.getpixel((-1, -1))
|
||||
|
||||
# check initial color
|
||||
im = Image.new(mode, (1, 1), c)
|
||||
assert (
|
||||
im.getpixel((0, 0)) == c
|
||||
), f"initial color failed for mode {mode}, color {c} "
|
||||
im = Image.new(mode, (1, 1), expected_color)
|
||||
actual_color = im.getpixel((0, 0))
|
||||
assert actual_color == expected_color, (
|
||||
f"initial color failed for mode {mode}, "
|
||||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# check initial color negative index
|
||||
assert (
|
||||
im.getpixel((-1, -1)) == c
|
||||
), f"initial color failed with negative index for mode {mode}, color {c} "
|
||||
actual_color = im.getpixel((-1, -1))
|
||||
assert actual_color == expected_color, (
|
||||
f"initial color failed with negative index for mode {mode}, "
|
||||
f"expected {expected_color} got {actual_color}"
|
||||
)
|
||||
|
||||
# Check 0
|
||||
im = Image.new(mode, (0, 0), c)
|
||||
im = Image.new(mode, (0, 0), expected_color)
|
||||
with pytest.raises(error):
|
||||
im.getpixel((0, 0))
|
||||
# Check 0 negative index
|
||||
|
@ -205,13 +214,13 @@ class TestImageGetPixel(AccessTest):
|
|||
self.check(mode)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||
def test_signedness(self, mode):
|
||||
@pytest.mark.parametrize(
|
||||
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
|
||||
)
|
||||
def test_signedness(self, mode, expected_color):
|
||||
# see https://github.com/python-pillow/Pillow/issues/452
|
||||
# pixelaccess is using signed int* instead of uint*
|
||||
self.check(mode, 2**15 - 1)
|
||||
self.check(mode, 2**15)
|
||||
self.check(mode, 2**15 + 1)
|
||||
self.check(mode, 2**16 - 1)
|
||||
self.check(mode, expected_color)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
|
||||
|
|
|
@ -351,7 +351,8 @@ def test_rotated_transposed_font(font, orientation):
|
|||
assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0]
|
||||
|
||||
# text length is undefined for vertical text
|
||||
pytest.raises(ValueError, draw.textlength, word)
|
||||
with pytest.raises(ValueError):
|
||||
draw.textlength(word)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -872,25 +873,23 @@ def test_anchor_invalid(font):
|
|||
d.font = font
|
||||
|
||||
for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]:
|
||||
pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor))
|
||||
pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor))
|
||||
pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor))
|
||||
pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor))
|
||||
pytest.raises(
|
||||
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
font.getmask2("hello", anchor=anchor)
|
||||
with pytest.raises(ValueError):
|
||||
font.getbbox("hello", anchor=anchor)
|
||||
with pytest.raises(ValueError):
|
||||
d.text((0, 0), "hello", anchor=anchor)
|
||||
with pytest.raises(ValueError):
|
||||
d.textbbox((0, 0), "hello", anchor=anchor)
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
|
||||
for anchor in ["lt", "lb"]:
|
||||
pytest.raises(
|
||||
ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor),
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_text((0, 0), "foo\nbar", anchor=anchor)
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bpp", (1, 2, 4, 8))
|
||||
|
|
|
@ -360,37 +360,20 @@ def test_anchor_invalid_ttb():
|
|||
d.font = font
|
||||
|
||||
for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]:
|
||||
pytest.raises(
|
||||
ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb")
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb")
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb")
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"),
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: d.multiline_text(
|
||||
(0, 0), "foo\nbar", anchor=anchor, direction="ttb"
|
||||
),
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: d.multiline_textbbox(
|
||||
(0, 0), "foo\nbar", anchor=anchor, direction="ttb"
|
||||
),
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
font.getmask2("hello", anchor=anchor, direction="ttb")
|
||||
with pytest.raises(ValueError):
|
||||
font.getbbox("hello", anchor=anchor, direction="ttb")
|
||||
with pytest.raises(ValueError):
|
||||
d.text((0, 0), "hello", anchor=anchor, direction="ttb")
|
||||
with pytest.raises(ValueError):
|
||||
d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb")
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb")
|
||||
# ttb multiline text does not support anchors at all
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"),
|
||||
)
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"),
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb")
|
||||
with pytest.raises(ValueError):
|
||||
d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb")
|
||||
|
|
|
@ -186,8 +186,8 @@ Many of Pillow's features require external libraries:
|
|||
* Pillow wheels since version 8.2.0 include a modified version of libraqm that
|
||||
loads libfribidi at runtime if it is installed.
|
||||
On Windows this requires compiling FriBiDi and installing ``fribidi.dll``
|
||||
into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs)
|
||||
<https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-desktop-applications>`_
|
||||
into a directory listed in the `Dynamic-link library search order (Microsoft Learn)
|
||||
<https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-unpackaged-apps>`_
|
||||
(``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected).
|
||||
See `Build Options`_ to see how to build this version.
|
||||
* Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime.
|
||||
|
|
|
@ -338,7 +338,7 @@ Methods
|
|||
:param fill: Color to use for the fill.
|
||||
:param width: The line width, in pixels.
|
||||
:param corners: A tuple of whether to round each corner,
|
||||
`(top_left, top_right, bottom_right, bottom_left)`.
|
||||
``(top_left, top_right, bottom_right, bottom_left)``.
|
||||
|
||||
.. versionadded:: 8.2.0
|
||||
|
||||
|
|
60
docs/releasenotes/9.5.0.rst
Normal file
60
docs/releasenotes/9.5.0.rst
Normal file
|
@ -0,0 +1,60 @@
|
|||
9.5.0
|
||||
-----
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
Added ``dpi`` argument when saving PDFs
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When saving a PDF, resolution could already be specified using the
|
||||
``resolution`` argument. Now, a tuple of ``(x_resolution, y_resolution)`` can
|
||||
be provided as ``dpi``. If both are provided, ``dpi`` will override
|
||||
``resolution``.
|
||||
|
||||
Added ``corners`` argument to ``ImageDraw.rounded_rectangle()``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of
|
||||
``corners``. This a tuple of Booleans, specifying whether to round each corner,
|
||||
``(top_left, top_right, bottom_right, bottom_left)``.
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
9.5.0
|
||||
9.4.0
|
||||
9.3.0
|
||||
9.2.0
|
||||
|
|
|
@ -1850,6 +1850,11 @@ def _save(im, fp, filename):
|
|||
fp.write(data)
|
||||
if errcode:
|
||||
break
|
||||
if _fp:
|
||||
try:
|
||||
os.close(_fp)
|
||||
except OSError:
|
||||
pass
|
||||
if errcode < 0:
|
||||
msg = f"encoder error {errcode} when writing image file"
|
||||
raise OSError(msg)
|
||||
|
|
|
@ -116,7 +116,7 @@ cms_profile_open(PyObject *self, PyObject *args) {
|
|||
}
|
||||
|
||||
static PyObject *
|
||||
cms_profile_fromstring(PyObject *self, PyObject *args) {
|
||||
cms_profile_frombytes(PyObject *self, PyObject *args) {
|
||||
cmsHPROFILE hProfile;
|
||||
|
||||
char *pProfile;
|
||||
|
@ -960,8 +960,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) {
|
|||
static PyMethodDef pyCMSdll_methods[] = {
|
||||
|
||||
{"profile_open", cms_profile_open, METH_VARARGS},
|
||||
{"profile_frombytes", cms_profile_fromstring, METH_VARARGS},
|
||||
{"profile_fromstring", cms_profile_fromstring, METH_VARARGS},
|
||||
{"profile_frombytes", cms_profile_frombytes, METH_VARARGS},
|
||||
{"profile_tobytes", cms_profile_tobytes, METH_VARARGS},
|
||||
|
||||
/* profile and transform functions */
|
||||
|
|
Loading…
Reference in New Issue
Block a user