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

This commit is contained in:
Andrew Murray 2022-04-25 11:00:09 +10:00 committed by GitHub
commit 853a95d56b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 689 additions and 481 deletions

View File

@ -40,12 +40,10 @@ if [[ $(uname) != CYGWIN* ]]; then
PYTHONOPTIMIZE=0 python3 -m pip install cffi PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install numpy python3 -m pip install numpy
# PyQt5 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
# arm64, ppc64le, s390x CPUs: sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0
# "ERROR: Could not find a version that satisfies the requirement pyqt5" python3 -m pip install pyqt6
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
python3 -m pip install pyqt5
fi fi
# webp # webp

View File

@ -27,6 +27,7 @@ jobs:
gentoo, gentoo,
ubuntu-18.04-bionic-amd64, ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
] ]
dockerTag: [main] dockerTag: [main]
include: include:

View File

@ -4,9 +4,11 @@ on:
- cron: "30 2 * * *" # daily at 02:30 UTC - cron: "30 2 * * *" # daily at 02:30 UTC
push: push:
paths: paths:
- "Pipfile*"
- ".github/workflows/tidelift.yml" - ".github/workflows/tidelift.yml"
pull_request: pull_request:
paths: paths:
- "Pipfile*"
- ".github/workflows/tidelift.yml" - ".github/workflows/tidelift.yml"
workflow_dispatch: workflow_dispatch:

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
9.2.0 (unreleased) 9.2.0 (unreleased)
------------------ ------------------
- Increase wait time of temporary file deletion on Windows #6224
[AlexTedeschi]
- Deprecate FreeTypeFont.getmask2 fill parameter #6220
[nulano, radarhere, hugovk]
- Round lut values where necessary #6188 - Round lut values where necessary #6188
[radarhere] [radarhere]

View File

@ -324,7 +324,7 @@ def is_mingw():
return sysconfig.get_platform() == "mingw" return sysconfig.get_platform() == "mingw"
class cached_property: class CachedProperty:
def __init__(self, func): def __init__(self, func):
self.func = func self.func = func

View File

@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1):
return image._new(image.im.box_blur(radius, n)) return image._new(image.im.box_blur(radius, n))
def assertImage(im, data, delta=0): def assert_image(im, data, delta=0):
it = iter(im.getdata()) it = iter(im.getdata())
for data_row in data: for data_row in data:
im_row = [next(it) for _ in range(im.size[0])] im_row = [next(it) for _ in range(im.size[0])]
@ -35,12 +35,12 @@ def assertImage(im, data, delta=0):
next(it) next(it)
def assertBlur(im, radius, data, passes=1, delta=0): def assert_blur(im, radius, data, passes=1, delta=0):
# check grayscale image # check grayscale image
assertImage(box_blur(im, radius, passes), data, delta) assert_image(box_blur(im, radius, passes), data, delta)
rgba = Image.merge("RGBA", (im, im, im, im)) rgba = Image.merge("RGBA", (im, im, im, im))
for band in box_blur(rgba, radius, passes).split(): for band in box_blur(rgba, radius, passes).split():
assertImage(band, data, delta) assert_image(band, data, delta)
def test_color_modes(): def test_color_modes():
@ -64,7 +64,7 @@ def test_color_modes():
def test_radius_0(): def test_radius_0():
assertBlur( assert_blur(
sample, sample,
0, 0,
[ [
@ -80,7 +80,7 @@ def test_radius_0():
def test_radius_0_02(): def test_radius_0_02():
assertBlur( assert_blur(
sample, sample,
0.02, 0.02,
[ [
@ -97,7 +97,7 @@ def test_radius_0_02():
def test_radius_0_05(): def test_radius_0_05():
assertBlur( assert_blur(
sample, sample,
0.05, 0.05,
[ [
@ -114,7 +114,7 @@ def test_radius_0_05():
def test_radius_0_1(): def test_radius_0_1():
assertBlur( assert_blur(
sample, sample,
0.1, 0.1,
[ [
@ -131,7 +131,7 @@ def test_radius_0_1():
def test_radius_0_5(): def test_radius_0_5():
assertBlur( assert_blur(
sample, sample,
0.5, 0.5,
[ [
@ -148,7 +148,7 @@ def test_radius_0_5():
def test_radius_1(): def test_radius_1():
assertBlur( assert_blur(
sample, sample,
1, 1,
[ [
@ -165,7 +165,7 @@ def test_radius_1():
def test_radius_1_5(): def test_radius_1_5():
assertBlur( assert_blur(
sample, sample,
1.5, 1.5,
[ [
@ -182,7 +182,7 @@ def test_radius_1_5():
def test_radius_bigger_then_half(): def test_radius_bigger_then_half():
assertBlur( assert_blur(
sample, sample,
3, 3,
[ [
@ -199,7 +199,7 @@ def test_radius_bigger_then_half():
def test_radius_bigger_then_width(): def test_radius_bigger_then_width():
assertBlur( assert_blur(
sample, sample,
10, 10,
[ [
@ -214,7 +214,7 @@ def test_radius_bigger_then_width():
def test_extreme_large_radius(): def test_extreme_large_radius():
assertBlur( assert_blur(
sample, sample,
600, 600,
[ [
@ -229,7 +229,7 @@ def test_extreme_large_radius():
def test_two_passes(): def test_two_passes():
assertBlur( assert_blur(
sample, sample,
1, 1,
[ [
@ -247,7 +247,7 @@ def test_two_passes():
def test_three_passes(): def test_three_passes():
assertBlur( assert_blur(
sample, sample,
1, 1,
[ [

View File

@ -15,27 +15,27 @@ except ImportError:
class TestColorLut3DCoreAPI: class TestColorLut3DCoreAPI:
def generate_identity_table(self, channels, size): def generate_identity_table(self, channels, size):
if isinstance(size, tuple): if isinstance(size, tuple):
size1D, size2D, size3D = size size_1d, size_2d, size_3d = size
else: else:
size1D, size2D, size3D = (size, size, size) size_1d, size_2d, size_3d = (size, size, size)
table = [ table = [
[ [
r / (size1D - 1) if size1D != 1 else 0, r / (size_1d - 1) if size_1d != 1 else 0,
g / (size2D - 1) if size2D != 1 else 0, g / (size_2d - 1) if size_2d != 1 else 0,
b / (size3D - 1) if size3D != 1 else 0, b / (size_3d - 1) if size_3d != 1 else 0,
r / (size1D - 1) if size1D != 1 else 0, r / (size_1d - 1) if size_1d != 1 else 0,
g / (size2D - 1) if size2D != 1 else 0, g / (size_2d - 1) if size_2d != 1 else 0,
][:channels] ][:channels]
for b in range(size3D) for b in range(size_3d)
for g in range(size2D) for g in range(size_2d)
for r in range(size1D) for r in range(size_1d)
] ]
return ( return (
channels, channels,
size1D, size_1d,
size2D, size_2d,
size3D, size_3d,
[item for sublist in table for item in sublist], [item for sublist in table for item in sublist],
) )

View File

@ -78,7 +78,7 @@ class TestDecompressionCrop:
def teardown_class(self): def teardown_class(self):
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def testEnlargeCrop(self): def test_enlarge_crop(self):
# Crops can extend the extents, therefore we should have the # Crops can extend the extents, therefore we should have the
# same decompression bomb warnings on them. # same decompression bomb warnings on them.
with hopper() as src: with hopper() as src:

91
Tests/test_deprecate.py Normal file
View File

@ -0,0 +1,91 @@
import pytest
from PIL import _deprecate
@pytest.mark.parametrize(
"version, expected",
[
(
10,
"Old thing is deprecated and will be removed in Pillow 10 "
r"\(2023-07-01\)\. Use new thing instead\.",
),
(
None,
r"Old thing is deprecated and will be removed in a future version\. "
r"Use new thing instead\.",
),
],
)
def test_version(version, expected):
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing")
def test_unknown_version():
expected = r"Unknown removal version, update PIL\._deprecate\?"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate("Old thing", 12345, "new thing")
@pytest.mark.parametrize(
"deprecated, plural, expected",
[
(
"Old thing",
False,
r"Old thing is deprecated and should be removed\.",
),
(
"Old things",
True,
r"Old things are deprecated and should be removed\.",
),
],
)
def test_old_version(deprecated, plural, expected):
expected = r""
with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural)
def test_plural():
expected = (
r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
r"Use new thing instead\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old things", 10, "new thing", plural=True)
def test_replacement_and_action():
expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate(
"Old thing", 10, replacement="new thing", action="Upgrade to new thing"
)
@pytest.mark.parametrize(
"action",
[
"Upgrade to new thing",
"Upgrade to new thing.",
],
)
def test_action(action):
expected = (
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
r"Upgrade to new thing\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 10, action=action)
def test_no_replacement_or_action():
expected = (
r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)"
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 10)

View File

@ -0,0 +1,18 @@
import warnings
with warnings.catch_warnings(record=True) as w:
# Arrange: cause all warnings to always be triggered
warnings.simplefilter("always")
# Act: trigger a warning with Qt5
from PIL import ImageQt
def test_deprecated():
# Assert
if ImageQt.qt_version in ("5", "side2"):
assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "deprecated" in str(w[0].message)
else:
assert len(w) == 0

View File

@ -799,31 +799,31 @@ def test_zero_comment_subblocks():
def test_version(tmp_path): def test_version(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
def assertVersionAfterSave(im, version): def assert_version_after_save(im, version):
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["version"] == version assert reread.info["version"] == version
# Test that GIF87a is used by default # Test that GIF87a is used by default
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
assertVersionAfterSave(im, b"GIF87a") assert_version_after_save(im, b"GIF87a")
# Test setting the version to 89a # Test setting the version to 89a
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.info["version"] = b"89a" im.info["version"] = b"89a"
assertVersionAfterSave(im, b"GIF89a") assert_version_after_save(im, b"GIF89a")
# Test that adding a GIF89a feature changes the version # Test that adding a GIF89a feature changes the version
im.info["transparency"] = 1 im.info["transparency"] = 1
assertVersionAfterSave(im, b"GIF89a") assert_version_after_save(im, b"GIF89a")
# Test that a GIF87a image is also saved in that format # Test that a GIF87a image is also saved in that format
with Image.open("Tests/images/test.colors.gif") as im: with Image.open("Tests/images/test.colors.gif") as im:
assertVersionAfterSave(im, b"GIF87a") assert_version_after_save(im, b"GIF87a")
# Test that a GIF89a image is also saved in that format # Test that a GIF89a image is also saved in that format
im.info["version"] = b"GIF89a" im.info["version"] = b"GIF89a"
assertVersionAfterSave(im, b"GIF87a") assert_version_after_save(im, b"GIF87a")
def test_append_images(tmp_path): def test_append_images(tmp_path):
@ -838,10 +838,10 @@ def test_append_images(tmp_path):
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending using a generator # Tests appending using a generator
def imGenerator(ims): def im_generator(ims):
yield from ims yield from ims
im.save(out, save_all=True, append_images=imGenerator(ims)) im.save(out, save_all=True, append_images=im_generator(ims))
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.n_frames == 3 assert reread.n_frames == 3

View File

@ -145,10 +145,10 @@ def test_mp_attribute():
for test_file in test_files: for test_file in test_files:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
frameNumber = 0 frame_number = 0
for mpentry in mpinfo[0xB002]: for mpentry in mpinfo[0xB002]:
mpattr = mpentry["Attribute"] mpattr = mpentry["Attribute"]
if frameNumber: if frame_number:
assert not mpattr["RepresentativeImageFlag"] assert not mpattr["RepresentativeImageFlag"]
else: else:
assert mpattr["RepresentativeImageFlag"] assert mpattr["RepresentativeImageFlag"]
@ -157,7 +157,7 @@ def test_mp_attribute():
assert mpattr["ImageDataFormat"] == "JPEG" assert mpattr["ImageDataFormat"] == "JPEG"
assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
assert mpattr["Reserved"] == 0 assert mpattr["Reserved"] == 0
frameNumber += 1 frame_number += 1
def test_seek(): def test_seek():

View File

@ -131,10 +131,10 @@ def test_save_all(tmp_path):
assert os.path.getsize(outfile) > 0 assert os.path.getsize(outfile) > 0
# Test appending using a generator # Test appending using a generator
def imGenerator(ims): def im_generator(ims):
yield from ims yield from ims
im.save(outfile, save_all=True, append_images=imGenerator(ims)) im.save(outfile, save_all=True, append_images=im_generator(ims))
assert os.path.isfile(outfile) assert os.path.isfile(outfile)
assert os.path.getsize(outfile) > 0 assert os.path.getsize(outfile) > 0
@ -253,9 +253,9 @@ def test_pdf_append(tmp_path):
check_pdf_pages_consistency(pdf) check_pdf_pages_consistency(pdf)
# append two images # append two images
mode_CMYK = hopper("CMYK") mode_cmyk = hopper("CMYK")
mode_P = hopper("P") mode_p = hopper("P")
mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p])
# open the PDF again, check pages and info again # open the PDF again, check pages and info again
with PdfParser.PdfParser(pdf_filename) as pdf: with PdfParser.PdfParser(pdf_filename) as pdf:

View File

@ -151,14 +151,14 @@ class TestFileTiff:
assert im.info["dpi"] == (71.0, 71.0) assert im.info["dpi"] == (71.0, 71.0)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"resolutionUnit, dpi", "resolution_unit, dpi",
[(None, 72.8), (2, 72.8), (3, 184.912)], [(None, 72.8), (2, 72.8), (3, 184.912)],
) )
def test_load_float_dpi(self, resolutionUnit, dpi): def test_load_float_dpi(self, resolution_unit, dpi):
with Image.open( with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif" "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im: ) as im:
assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
assert im.info["dpi"] == (dpi, dpi) assert im.info["dpi"] == (dpi, dpi)
def test_save_float_dpi(self, tmp_path): def test_save_float_dpi(self, tmp_path):
@ -655,11 +655,11 @@ class TestFileTiff:
assert reread.n_frames == 3 assert reread.n_frames == 3
# Test appending using a generator # Test appending using a generator
def imGenerator(ims): def im_generator(ims):
yield from ims yield from ims
mp = BytesIO() mp = BytesIO()
im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
mp.seek(0, os.SEEK_SET) mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread: with Image.open(mp) as reread:

View File

@ -28,26 +28,26 @@ def test_rt_metadata(tmp_path):
# For text items, we still have to decode('ascii','replace') because # For text items, we still have to decode('ascii','replace') because
# the tiff file format can't take 8 bit bytes in that field. # the tiff file format can't take 8 bit bytes in that field.
basetextdata = "This is some arbitrary metadata for a text field" base_text_data = "This is some arbitrary metadata for a text field"
bindata = basetextdata.encode("ascii") + b" \xff" bin_data = base_text_data.encode("ascii") + b" \xff"
textdata = basetextdata + " " + chr(255) text_data = base_text_data + " " + chr(255)
reloaded_textdata = basetextdata + " ?" reloaded_text_data = base_text_data + " ?"
floatdata = 12.345 float_data = 12.345
doubledata = 67.89 double_data = 67.89
info = TiffImagePlugin.ImageFileDirectory() info = TiffImagePlugin.ImageFileDirectory()
ImageJMetaData = TAG_IDS["ImageJMetaData"] ImageJMetaData = TAG_IDS["ImageJMetaData"]
ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
ImageDescription = TAG_IDS["ImageDescription"] ImageDescription = TAG_IDS["ImageDescription"]
info[ImageJMetaDataByteCounts] = len(bindata) info[ImageJMetaDataByteCounts] = len(bin_data)
info[ImageJMetaData] = bindata info[ImageJMetaData] = bin_data
info[TAG_IDS["RollAngle"]] = floatdata info[TAG_IDS["RollAngle"]] = float_data
info.tagtype[TAG_IDS["RollAngle"]] = 11 info.tagtype[TAG_IDS["RollAngle"]] = 11
info[TAG_IDS["YawAngle"]] = doubledata info[TAG_IDS["YawAngle"]] = double_data
info.tagtype[TAG_IDS["YawAngle"]] = 12 info.tagtype[TAG_IDS["YawAngle"]] = 12
info[ImageDescription] = textdata info[ImageDescription] = text_data
f = str(tmp_path / "temp.tif") f = str(tmp_path / "temp.tif")
@ -55,28 +55,28 @@ def test_rt_metadata(tmp_path):
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
assert loaded.tag[ImageJMetaData] == bindata assert loaded.tag[ImageJMetaData] == bin_data
assert loaded.tag_v2[ImageJMetaData] == bindata assert loaded.tag_v2[ImageJMetaData] == bin_data
assert loaded.tag[ImageDescription] == (reloaded_textdata,) assert loaded.tag[ImageDescription] == (reloaded_text_data,)
assert loaded.tag_v2[ImageDescription] == reloaded_textdata assert loaded.tag_v2[ImageDescription] == reloaded_text_data
loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
assert round(abs(loaded_float - floatdata), 5) == 0 assert round(abs(loaded_float - float_data), 5) == 0
loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0] loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
assert round(abs(loaded_double - doubledata), 7) == 0 assert round(abs(loaded_double - double_data), 7) == 0
# check with 2 element ImageJMetaDataByteCounts, issue #2006 # check with 2 element ImageJMetaDataByteCounts, issue #2006
info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
img.save(f, tiffinfo=info) img.save(f, tiffinfo=info)
with Image.open(f) as loaded: with Image.open(f) as loaded:
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata(): def test_read_metadata():
@ -356,7 +356,7 @@ def test_empty_values():
assert 33432 in info assert 33432 in info
def test_PhotoshopInfo(tmp_path): def test_photoshop_info(tmp_path):
with Image.open("Tests/images/issue_2278.tif") as im: with Image.open("Tests/images/issue_2278.tif") as im:
assert len(im.tag_v2[34377]) == 70 assert len(im.tag_v2[34377]) == 70
assert isinstance(im.tag_v2[34377], bytes) assert isinstance(im.tag_v2[34377], bytes)

View File

@ -90,14 +90,14 @@ def test_write_animation_RGB(tmp_path):
check(temp_file1) check(temp_file1)
# Tests appending using a generator # Tests appending using a generator
def imGenerator(ims): def im_generator(ims):
yield from ims yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp") temp_file2 = str(tmp_path / "temp_generator.webp")
frame1.copy().save( frame1.copy().save(
temp_file2, temp_file2,
save_all=True, save_all=True,
append_images=imGenerator([frame2]), append_images=im_generator([frame2]),
lossless=True, lossless=True,
) )
check(temp_file2) check(temp_file2)

View File

@ -6,8 +6,8 @@ from .helper import hopper
def test_copy(): def test_copy():
croppedCoordinates = (10, 10, 20, 20) cropped_coordinates = (10, 10, 20, 20)
croppedSize = (10, 10) cropped_size = (10, 10)
for mode in "1", "P", "L", "RGB", "I", "F": for mode in "1", "P", "L", "RGB", "I", "F":
# Internal copy method # Internal copy method
im = hopper(mode) im = hopper(mode)
@ -23,15 +23,15 @@ def test_copy():
# Internal copy method on a cropped image # Internal copy method on a cropped image
im = hopper(mode) im = hopper(mode)
out = im.crop(croppedCoordinates).copy() out = im.crop(cropped_coordinates).copy()
assert out.mode == im.mode assert out.mode == im.mode
assert out.size == croppedSize assert out.size == cropped_size
# Python's copy method on a cropped image # Python's copy method on a cropped image
im = hopper(mode) im = hopper(mode)
out = copy.copy(im.crop(croppedCoordinates)) out = copy.copy(im.crop(cropped_coordinates))
assert out.mode == im.mode assert out.mode == im.mode
assert out.size == croppedSize assert out.size == cropped_size
def test_copy_zero(): def test_copy_zero():

View File

@ -99,10 +99,10 @@ def test_rankfilter_properties():
def test_builtinfilter_p(): def test_builtinfilter_p():
builtinFilter = ImageFilter.BuiltinFilter() builtin_filter = ImageFilter.BuiltinFilter()
with pytest.raises(ValueError): with pytest.raises(ValueError):
builtinFilter.filter(hopper("P")) builtin_filter.filter(hopper("P"))
def test_kernel_not_enough_coefficients(): def test_kernel_not_enough_coefficients():

View File

@ -1,6 +1,12 @@
import warnings
import pytest import pytest
from PIL import Image, ImageQt from PIL import Image
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper

View File

@ -1,6 +1,6 @@
from PIL import Image from PIL import Image
from .helper import assert_image_equal, cached_property from .helper import CachedProperty, assert_image_equal
class TestImagingPaste: class TestImagingPaste:
@ -34,7 +34,7 @@ class TestImagingPaste:
im.paste(im2, mask) im.paste(im2, mask)
self.assert_9points_image(im, expected) self.assert_9points_image(im, expected)
@cached_property @CachedProperty
def mask_1(self): def mask_1(self):
mask = Image.new("1", (self.size, self.size)) mask = Image.new("1", (self.size, self.size))
px = mask.load() px = mask.load()
@ -43,11 +43,11 @@ class TestImagingPaste:
px[y, x] = (x + y) % 2 px[y, x] = (x + y) % 2
return mask return mask
@cached_property @CachedProperty
def mask_L(self): def mask_L(self):
return self.gradient_L.transpose(Image.Transpose.ROTATE_270) return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@cached_property @CachedProperty
def gradient_L(self): def gradient_L(self):
gradient = Image.new("L", (self.size, self.size)) gradient = Image.new("L", (self.size, self.size))
px = gradient.load() px = gradient.load()
@ -56,7 +56,7 @@ class TestImagingPaste:
px[y, x] = (x + y) % 255 px[y, x] = (x + y) % 255
return gradient return gradient
@cached_property @CachedProperty
def gradient_RGB(self): def gradient_RGB(self):
return Image.merge( return Image.merge(
"RGB", "RGB",
@ -67,7 +67,7 @@ class TestImagingPaste:
], ],
) )
@cached_property @CachedProperty
def gradient_LA(self): def gradient_LA(self):
return Image.merge( return Image.merge(
"LA", "LA",
@ -77,7 +77,7 @@ class TestImagingPaste:
], ],
) )
@cached_property @CachedProperty
def gradient_RGBA(self): def gradient_RGBA(self):
return Image.merge( return Image.merge(
"RGBA", "RGBA",
@ -89,7 +89,7 @@ class TestImagingPaste:
], ],
) )
@cached_property @CachedProperty
def gradient_RGBa(self): def gradient_RGBa(self):
return Image.merge( return Image.merge(
"RGBa", "RGBa",

View File

@ -12,6 +12,7 @@ from .helper import (
assert_image_equal_tofile, assert_image_equal_tofile,
assert_image_similar, assert_image_similar,
hopper, hopper,
skip_unless_feature,
) )
@ -264,6 +265,7 @@ class TestImageResize:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.resize((10, 10), "unknown") im.resize((10, 10), "unknown")
@skip_unless_feature("libtiff")
def test_load_first(self): def test_load_first(self):
# load() may change the size of the image # load() may change the size of the image
# Test that resize() is calling it before getting the size # Test that resize() is calling it before getting the size

View File

@ -7,6 +7,7 @@ from .helper import (
assert_image_similar, assert_image_similar,
fromstring, fromstring,
hopper, hopper,
skip_unless_feature,
tostring, tostring,
) )
@ -88,6 +89,7 @@ def test_no_resize():
assert im.size == (64, 64) assert im.size == (64, 64)
@skip_unless_feature("libtiff")
def test_load_first(): def test_load_first():
# load() may change the size of the image # load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations # Test that thumbnail() is calling it before performing size calculations

View File

@ -35,9 +35,9 @@ class TestImageFile:
parser = ImageFile.Parser() parser = ImageFile.Parser()
parser.feed(data) parser.feed(data)
imOut = parser.close() im_out = parser.close()
return im, imOut return im, im_out
assert_image_equal(*roundtrip("BMP")) assert_image_equal(*roundtrip("BMP"))
im1, im2 = roundtrip("GIF") im1, im2 = roundtrip("GIF")

View File

@ -977,6 +977,14 @@ class TestImageFont:
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
def test_fill_deprecation(self):
font = self.get_font()
with pytest.warns(DeprecationWarning):
font.getmask2("Hello world", fill=Image.core.fill)
with pytest.warns(DeprecationWarning):
with pytest.raises(TypeError):
font.getmask2("Hello world", fill=None)
@skip_unless_feature("raqm") @skip_unless_feature("raqm")
class TestImageFont_RaqmLayout(TestImageFont): class TestImageFont_RaqmLayout(TestImageFont):

View File

@ -48,8 +48,8 @@ def img_string_normalize(im):
return img_to_string(string_to_img(im)) return img_to_string(string_to_img(im))
def assert_img_equal_img_string(A, Bstring): def assert_img_equal_img_string(a, b_string):
assert img_to_string(A) == img_string_normalize(Bstring) assert img_to_string(a) == img_string_normalize(b_string)
def test_str_to_img(): def test_str_to_img():

View File

@ -174,7 +174,7 @@ def test_overflow_segfault():
# through to the sequence. Seeing this on 32-bit Windows. # through to the sequence. Seeing this on 32-bit Windows.
with pytest.raises((TypeError, MemoryError)): with pytest.raises((TypeError, MemoryError)):
# post patch, this fails with a memory error # post patch, this fails with a memory error
x = evil() x = Evil()
# This fails due to the invalid malloc above, # This fails due to the invalid malloc above,
# and segfaults # and segfaults
@ -182,7 +182,7 @@ def test_overflow_segfault():
x[i] = b"0" * 16 x[i] = b"0" * 16
class evil: class Evil:
def __init__(self): def __init__(self):
self.corrupt = Image.core.path(0x4000000000000000) self.corrupt = Image.core.path(0x4000000000000000)

View File

@ -2,10 +2,13 @@ import warnings
import pytest import pytest
from PIL import ImageQt
from .helper import assert_image_similar, hopper from .helper import assert_image_similar, hopper
with warnings.catch_warnings() as w:
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed" not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
) )

View File

@ -65,12 +65,12 @@ def test_libtiff():
def test_consecutive(): def test_consecutive():
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
firstFrame = None first_frame = None
for frame in ImageSequence.Iterator(im): for frame in ImageSequence.Iterator(im):
if firstFrame is None: if first_frame is None:
firstFrame = frame.copy() first_frame = frame.copy()
for frame in ImageSequence.Iterator(im): for frame in ImageSequence.Iterator(im):
assert_image_equal(frame, firstFrame) assert_image_equal(frame, first_frame)
break break

View File

@ -26,51 +26,51 @@ def test_basic(tmp_path):
def basic(mode): def basic(mode):
imIn = original.convert(mode) im_in = original.convert(mode)
verify(imIn) verify(im_in)
w, h = imIn.size w, h = im_in.size
imOut = imIn.copy() im_out = im_in.copy()
verify(imOut) # copy verify(im_out) # copy
imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
verify(imOut) # transform verify(im_out) # transform
filename = str(tmp_path / "temp.im") filename = str(tmp_path / "temp.im")
imIn.save(filename) im_in.save(filename)
with Image.open(filename) as imOut: with Image.open(filename) as im_out:
verify(imIn) verify(im_in)
verify(imOut) verify(im_out)
imOut = imIn.crop((0, 0, w, h)) im_out = im_in.crop((0, 0, w, h))
verify(imOut) verify(im_out)
imOut = Image.new(mode, (w, h), None) im_out = Image.new(mode, (w, h), None)
imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
verify(imIn) verify(im_in)
verify(imOut) verify(im_out)
imIn = Image.new(mode, (1, 1), 1) im_in = Image.new(mode, (1, 1), 1)
assert imIn.getpixel((0, 0)) == 1 assert im_in.getpixel((0, 0)) == 1
imIn.putpixel((0, 0), 2) im_in.putpixel((0, 0), 2)
assert imIn.getpixel((0, 0)) == 2 assert im_in.getpixel((0, 0)) == 2
if mode == "L": if mode == "L":
maximum = 255 maximum = 255
else: else:
maximum = 32767 maximum = 32767
imIn = Image.new(mode, (1, 1), 256) im_in = Image.new(mode, (1, 1), 256)
assert imIn.getpixel((0, 0)) == min(256, maximum) assert im_in.getpixel((0, 0)) == min(256, maximum)
imIn.putpixel((0, 0), 512) im_in.putpixel((0, 0), 512)
assert imIn.getpixel((0, 0)) == min(512, maximum) assert im_in.getpixel((0, 0)) == min(512, maximum)
basic("L") basic("L")

View File

@ -1,6 +1,10 @@
import warnings
import pytest import pytest
from PIL import ImageQt with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper from .helper import assert_image_equal, assert_image_equal_tofile, hopper

View File

@ -1,6 +1,10 @@
import warnings
import pytest import pytest
from PIL import ImageQt with warnings.catch_warnings():
warnings.simplefilter("ignore", category=DeprecationWarning)
from PIL import ImageQt
from .helper import assert_image_equal, assert_image_equal_tofile, hopper from .helper import assert_image_equal, assert_image_equal_tofile, hopper

View File

@ -8,7 +8,7 @@ def test_is_path():
fp = "filename.ext" fp = "filename.ext"
# Act # Act
it_is = _util.isPath(fp) it_is = _util.is_path(fp)
# Assert # Assert
assert it_is assert it_is
@ -21,7 +21,7 @@ def test_path_obj_is_path():
test_path = Path("filename.ext") test_path = Path("filename.ext")
# Act # Act
it_is = _util.isPath(test_path) it_is = _util.is_path(test_path)
# Assert # Assert
assert it_is assert it_is
@ -33,7 +33,7 @@ def test_is_not_path(tmp_path):
pass pass
# Act # Act
it_is_not = _util.isPath(fp) it_is_not = _util.is_path(fp)
# Assert # Assert
assert not it_is_not assert not it_is_not
@ -44,7 +44,7 @@ def test_is_directory():
directory = "Tests" directory = "Tests"
# Act # Act
it_is = _util.isDirectory(directory) it_is = _util.is_directory(directory)
# Assert # Assert
assert it_is assert it_is
@ -55,7 +55,7 @@ def test_is_not_directory():
text = "abc" text = "abc"
# Act # Act
it_is_not = _util.isDirectory(text) it_is_not = _util.is_directory(text)
# Assert # Assert
assert not it_is_not assert not it_is_not
@ -65,7 +65,7 @@ def test_deferred_error():
# Arrange # Arrange
# Act # Act
thing = _util.deferred_error(ValueError("Some error text")) thing = _util.DeferredError(ValueError("Some error text"))
# Assert # Assert
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@ -142,6 +142,14 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
:mod:`~PIL.FitsImagePlugin` instead. :mod:`~PIL.FitsImagePlugin` instead.
FreeTypeFont.getmask2 fill parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
deprecated and will be removed in Pillow 10 (2023-07-01).
PhotoImage.paste box parameter PhotoImage.paste box parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -149,6 +157,19 @@ PhotoImage.paste box parameter
The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
PyQt5 and PySide2
~~~~~~~~~~~~~~~~~
.. deprecated:: 9.2.0
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
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.
Removed features Removed features
---------------- ----------------

View File

@ -101,7 +101,7 @@ GIF
^^^ ^^^
Pillow reads GIF87a and GIF89a versions of the GIF file format. The library Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
writes run-length encoded files in GIF87a by default, unless GIF89a features writes LZW encoded files in GIF87a by default, unless GIF89a features
are used or GIF89a is already in use. are used or GIF89a is already in use.
GIF files are initially read as grayscale (``L``) or palette mode (``P``) GIF files are initially read as grayscale (``L``) or palette mode (``P``)

View File

@ -504,6 +504,17 @@ image header. In addition, seek will also be used when the image data is read
tar file, you can use the :py:class:`~PIL.ContainerIO` or tar file, you can use the :py:class:`~PIL.ContainerIO` or
:py:class:`~PIL.TarIO` modules to access it. :py:class:`~PIL.TarIO` modules to access it.
Reading from URL
^^^^^^^^^^^^^^^^
::
from PIL import Image
from urllib.request import urlopen
url = "https://python-pillow.org/images/pillow-logo.png"
img = Image.open(urlopen(url))
Reading from a tar archive Reading from a tar archive
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -474,6 +474,8 @@ These platforms are built and tested for every change.
| | 3.8 | arm64v8, ppc64le, | | | 3.8 | arm64v8, ppc64le, |
| | | s390x | | | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.7 | x86-64 | | Windows Server 2016 | 3.7 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 | | Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 |

View File

@ -7,6 +7,14 @@
The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5 The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5
or PySide2 QImage objects from PIL images. or PySide2 QImage objects from PIL images.
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
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.
.. versionadded:: 1.1.6 .. versionadded:: 1.1.6
.. py:class:: ImageQt(image) .. py:class:: ImageQt(image)

View File

@ -9,6 +9,14 @@ Internal Modules
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL._deprecate` Module
-----------------------------
.. automodule:: PIL._deprecate
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL._tkinter_finder` Module :mod:`~PIL._tkinter_finder` Module
---------------------------------- ----------------------------------

View File

@ -0,0 +1,64 @@
9.2.0
-----
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
PyQt5 and PySide2
^^^^^^^^^^^^^^^^^
.. deprecated:: 9.2.0
`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
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.
FreeTypeFont.getmask2 fill parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 9.2.0
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2`
has been deprecated and will be removed in Pillow 10 (2023-07-01).
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
TODO
^^^^
TODO
Security
========
TODO
^^^^
TODO
Other Changes
=============
TODO
^^^^
TODO

View File

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

View File

@ -31,11 +31,11 @@ BLP files come in many different flavours:
import os import os
import struct import struct
import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._deprecate import deprecate
class Format(IntEnum): class Format(IntEnum):
@ -55,7 +55,6 @@ class AlphaEncoding(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in { for enum, prefix in {
Format: "BLP_FORMAT_", Format: "BLP_FORMAT_",
Encoding: "BLP_ENCODING_", Encoding: "BLP_ENCODING_",
@ -64,19 +63,7 @@ def __getattr__(name):
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -9,9 +9,8 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
import warnings
from . import FitsImagePlugin, Image, ImageFile from . import FitsImagePlugin, Image, ImageFile
from ._deprecate import deprecate
_handler = None _handler = None
@ -25,11 +24,11 @@ def register_handler(handler):
global _handler global _handler
_handler = handler _handler = handler
warnings.warn( deprecate(
"FitsStubImagePlugin is deprecated and will be removed in Pillow " "FitsStubImagePlugin",
"10 (2023-07-01). FITS images can now be read without a handler through " 10,
"FitsImagePlugin instead.", action="FITS images can now be read without "
DeprecationWarning, "a handler through FitsImagePlugin instead",
) )
# Override FitsImagePlugin with this handler # Override FitsImagePlugin with this handler

View File

@ -52,11 +52,11 @@ Note: All data is stored in little-Endian (Intel) byte order.
""" """
import struct import struct
import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._deprecate import deprecate
MAGIC = b"FTEX" MAGIC = b"FTEX"
@ -67,24 +67,11 @@ class Format(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Format: "FORMAT_"}.items(): for enum, prefix in {Format: "FORMAT_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -54,20 +54,25 @@ class GdImageFile(ImageFile.ImageFile):
self.mode = "L" # FIXME: "P" self.mode = "L" # FIXME: "P"
self._size = i16(s, 2), i16(s, 4) self._size = i16(s, 2), i16(s, 4)
trueColor = s[6] true_color = s[6]
trueColorOffset = 2 if trueColor else 0 true_color_offset = 2 if true_color else 0
# transparency index # transparency index
tindex = i32(s, 7 + trueColorOffset) tindex = i32(s, 7 + true_color_offset)
if tindex < 256: if tindex < 256:
self.info["transparency"] = tindex self.info["transparency"] = tindex
self.palette = ImagePalette.raw( self.palette = ImagePalette.raw(
"XBGR", s[7 + trueColorOffset + 4 : 7 + trueColorOffset + 4 + 256 * 4] "XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4]
) )
self.tile = [ self.tile = [
("raw", (0, 0) + self.size, 7 + trueColorOffset + 4 + 256 * 4, ("L", 0, 1)) (
"raw",
(0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4,
("L", 0, 1),
)
] ]

View File

@ -883,10 +883,10 @@ def _get_palette_bytes(im):
return im.palette.palette return im.palette.palette
def _get_background(im, infoBackground): def _get_background(im, info_background):
background = 0 background = 0
if infoBackground: if info_background:
background = infoBackground background = info_background
if isinstance(background, tuple): if isinstance(background, tuple):
# WebPImagePlugin stores an RGBA value in info["background"] # WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's # So it must be converted to the same format as GifImagePlugin's

View File

@ -50,28 +50,17 @@ except ImportError:
# Use __version__ instead. # Use __version__ instead.
from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._util import deferred_error, isPath from ._deprecate import deprecate
from ._util import DeferredError, is_path
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
if name in categories: if name in categories:
warnings.warn( deprecate("Image categories", 10, "is_animated", plural=True)
"Image categories are " + deprecated + "Use is_animated instead.",
DeprecationWarning,
stacklevel=2,
)
return categories[name] return categories[name]
elif name in ("NEAREST", "NONE"): elif name in ("NEAREST", "NONE"):
warnings.warn( deprecate(name, 10, "Resampling.NEAREST or Dither.NONE")
name
+ " is "
+ deprecated
+ "Use Resampling.NEAREST or Dither.NONE instead.",
DeprecationWarning,
stacklevel=2,
)
return 0 return 0
old_resampling = { old_resampling = {
"LINEAR": "BILINEAR", "LINEAR": "BILINEAR",
@ -79,31 +68,11 @@ def __getattr__(name):
"ANTIALIAS": "LANCZOS", "ANTIALIAS": "LANCZOS",
} }
if name in old_resampling: if name in old_resampling:
warnings.warn( deprecate(name, 10, f"Resampling.{old_resampling[name]}")
name
+ " is "
+ deprecated
+ "Use Resampling."
+ old_resampling[name]
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return Resampling[old_resampling[name]] return Resampling[old_resampling[name]]
for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize):
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(name, 10, f"{enum.__name__}.{name}")
name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@ -139,7 +108,7 @@ try:
) )
except ImportError as v: except ImportError as v:
core = deferred_error(ImportError("The _imaging C module is not installed.")) core = DeferredError(ImportError("The _imaging C module is not installed."))
# Explanations for ways that we know we might have an import error # Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"): if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for # The _imaging C module is present, but not compiled for
@ -538,12 +507,7 @@ class Image:
def __getattr__(self, name): def __getattr__(self, name):
if name == "category": if name == "category":
warnings.warn( deprecate("Image categories", 10, "is_animated", plural=True)
"Image categories are deprecated and will be removed in Pillow 10 "
"(2023-07-01). Use is_animated instead.",
DeprecationWarning,
stacklevel=2,
)
return self._category return self._category
raise AttributeError(name) raise AttributeError(name)
@ -613,7 +577,7 @@ class Image:
# Instead of simply setting to None, we're setting up a # Instead of simply setting to None, we're setting up a
# deferred error that will better explain that the core image # deferred error that will better explain that the core image
# object is gone. # object is gone.
self.im = deferred_error(ValueError("Operation on closed image")) self.im = DeferredError(ValueError("Operation on closed image"))
def _copy(self): def _copy(self):
self.load() self.load()
@ -2251,7 +2215,7 @@ class Image:
if isinstance(fp, Path): if isinstance(fp, Path):
filename = str(fp) filename = str(fp)
open_fp = True open_fp = True
elif isPath(fp): elif is_path(fp):
filename = fp filename = fp
open_fp = True open_fp = True
elif fp == sys.stdout: elif fp == sys.stdout:
@ -2259,7 +2223,7 @@ class Image:
fp = sys.stdout.buffer fp = sys.stdout.buffer
except AttributeError: except AttributeError:
pass pass
if not filename and hasattr(fp, "name") and isPath(fp.name): if not filename and hasattr(fp, "name") and is_path(fp.name):
# only set the name for metadata purposes # only set the name for metadata purposes
filename = fp.name filename = fp.name
@ -3065,7 +3029,7 @@ def open(fp, mode="r", formats=None):
filename = "" filename = ""
if isinstance(fp, Path): if isinstance(fp, Path):
filename = str(fp.resolve()) filename = str(fp.resolve())
elif isPath(fp): elif is_path(fp):
filename = fp filename = fp
if filename: if filename:
@ -3363,7 +3327,7 @@ def effect_mandelbrot(size, extent, quality):
:param size: The requested size in pixels, as a 2-tuple: :param size: The requested size in pixels, as a 2-tuple:
(width, height). (width, height).
:param extent: The extent to cover, as a 4-tuple: :param extent: The extent to cover, as a 4-tuple:
(x0, y0, x1, y2). (x0, y0, x1, y1).
:param quality: Quality. :param quality: Quality.
""" """
return Image()._new(core.effect_mandelbrot(size, extent, quality)) return Image()._new(core.effect_mandelbrot(size, extent, quality))

View File

@ -16,19 +16,20 @@
# below for the original description. # below for the original description.
import sys import sys
import warnings
from enum import IntEnum from enum import IntEnum
from PIL import Image from PIL import Image
from ._deprecate import deprecate
try: try:
from PIL import _imagingcms from PIL import _imagingcms
except ImportError as ex: except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing # Allow error import for doc purposes, but error out when accessing
# anything in core. # anything in core.
from ._util import deferred_error from ._util import DeferredError
_imagingcms = deferred_error(ex) _imagingcms = DeferredError(ex)
DESCRIPTION = """ DESCRIPTION = """
pyCMS pyCMS
@ -117,24 +118,11 @@ class Direction(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -197,18 +197,18 @@ class ImageDraw:
if width > 8: if width > 8:
# Cover potential gaps between the line and the joint # Cover potential gaps between the line and the joint
if flipped: if flipped:
gapCoords = [ gap_coords = [
coord_at_angle(point, angles[0] + 90), coord_at_angle(point, angles[0] + 90),
point, point,
coord_at_angle(point, angles[1] + 90), coord_at_angle(point, angles[1] + 90),
] ]
else: else:
gapCoords = [ gap_coords = [
coord_at_angle(point, angles[0] - 90), coord_at_angle(point, angles[0] - 90),
point, point,
coord_at_angle(point, angles[1] - 90), coord_at_angle(point, angles[1] - 90),
] ]
self.line(gapCoords, fill, width=3) self.line(gap_coords, fill, width=3)
def shape(self, shape, fill=None, outline=None): def shape(self, shape, fill=None, outline=None):
"""(Experimental) Draw a shape.""" """(Experimental) Draw a shape."""

View File

@ -33,7 +33,7 @@ import struct
import sys import sys
from . import Image from . import Image
from ._util import isPath from ._util import is_path
MAXBLOCK = 65536 MAXBLOCK = 65536
@ -99,7 +99,7 @@ class ImageFile(Image.Image):
self.decoderconfig = () self.decoderconfig = ()
self.decodermaxblock = MAXBLOCK self.decodermaxblock = MAXBLOCK
if isPath(fp): if is_path(fp):
# filename # filename
self.fp = open(fp, "rb") self.fp = open(fp, "rb")
self.filename = fp self.filename = fp

View File

@ -421,8 +421,8 @@ class Color3DLUT(MultibandFilter):
except TypeError: except TypeError:
size = (size, size, size) size = (size, size, size)
size = [int(x) for x in size] size = [int(x) for x in size]
for size1D in size: for size_1d in size:
if not 2 <= size1D <= 65: if not 2 <= size_1d <= 65:
raise ValueError("Size should be in [2, 65] range.") raise ValueError("Size should be in [2, 65] range.")
return size return size
@ -439,22 +439,22 @@ class Color3DLUT(MultibandFilter):
:param target_mode: Passed to the constructor of the resulting :param target_mode: Passed to the constructor of the resulting
lookup table. lookup table.
""" """
size1D, size2D, size3D = cls._check_size(size) size_1d, size_2d, size_3d = cls._check_size(size)
if channels not in (3, 4): if channels not in (3, 4):
raise ValueError("Only 3 or 4 output channels are supported") raise ValueError("Only 3 or 4 output channels are supported")
table = [0] * (size1D * size2D * size3D * channels) table = [0] * (size_1d * size_2d * size_3d * channels)
idx_out = 0 idx_out = 0
for b in range(size3D): for b in range(size_3d):
for g in range(size2D): for g in range(size_2d):
for r in range(size1D): for r in range(size_1d):
table[idx_out : idx_out + channels] = callback( table[idx_out : idx_out + channels] = callback(
r / (size1D - 1), g / (size2D - 1), b / (size3D - 1) r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1)
) )
idx_out += channels idx_out += channels
return cls( return cls(
(size1D, size2D, size3D), (size_1d, size_2d, size_3d),
table, table,
channels=channels, channels=channels,
target_mode=target_mode, target_mode=target_mode,
@ -484,20 +484,20 @@ class Color3DLUT(MultibandFilter):
raise ValueError("Only 3 or 4 output channels are supported") raise ValueError("Only 3 or 4 output channels are supported")
ch_in = self.channels ch_in = self.channels
ch_out = channels or ch_in ch_out = channels or ch_in
size1D, size2D, size3D = self.size size_1d, size_2d, size_3d = self.size
table = [0] * (size1D * size2D * size3D * ch_out) table = [0] * (size_1d * size_2d * size_3d * ch_out)
idx_in = 0 idx_in = 0
idx_out = 0 idx_out = 0
for b in range(size3D): for b in range(size_3d):
for g in range(size2D): for g in range(size_2d):
for r in range(size1D): for r in range(size_1d):
values = self.table[idx_in : idx_in + ch_in] values = self.table[idx_in : idx_in + ch_in]
if with_normals: if with_normals:
values = callback( values = callback(
r / (size1D - 1), r / (size_1d - 1),
g / (size2D - 1), g / (size_2d - 1),
b / (size3D - 1), b / (size_3d - 1),
*values, *values,
) )
else: else:

View File

@ -33,7 +33,8 @@ from enum import IntEnum
from io import BytesIO from io import BytesIO
from . import Image from . import Image
from ._util import isDirectory, isPath from ._deprecate import deprecate
from ._util import is_directory, is_path
class Layout(IntEnum): class Layout(IntEnum):
@ -42,29 +43,16 @@ class Layout(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Layout: "LAYOUT_"}.items(): for enum, prefix in {Layout: "LAYOUT_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
class _imagingft_not_installed: class _ImagingFtNotInstalled:
# module placeholder # module placeholder
def __getattr__(self, id): def __getattr__(self, id):
raise ImportError("The _imagingft C module is not installed") raise ImportError("The _imagingft C module is not installed")
@ -73,7 +61,10 @@ class _imagingft_not_installed:
try: try:
from . import _imagingft as core from . import _imagingft as core
except ImportError: except ImportError:
core = _imagingft_not_installed() core = _ImagingFtNotInstalled()
_UNSPECIFIED = object()
# FIXME: add support for pilfont2 format (see FontFile.py) # FIXME: add support for pilfont2 format (see FontFile.py)
@ -196,8 +187,6 @@ class FreeTypeFont:
if core.HAVE_RAQM: if core.HAVE_RAQM:
layout_engine = Layout.RAQM layout_engine = Layout.RAQM
elif layout_engine == Layout.RAQM and not core.HAVE_RAQM: elif layout_engine == Layout.RAQM and not core.HAVE_RAQM:
import warnings
warnings.warn( warnings.warn(
"Raqm layout was requested, but Raqm is not available. " "Raqm layout was requested, but Raqm is not available. "
"Falling back to basic layout." "Falling back to basic layout."
@ -212,7 +201,7 @@ class FreeTypeFont:
"", size, index, encoding, self.font_bytes, layout_engine "", size, index, encoding, self.font_bytes, layout_engine
) )
if isPath(font): if is_path(font):
if sys.platform == "win32": if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode() font_bytes_path = font if isinstance(font, bytes) else font.encode()
try: try:
@ -616,7 +605,7 @@ class FreeTypeFont:
self, self,
text, text,
mode="", mode="",
fill=Image.core.fill, fill=_UNSPECIFIED,
direction=None, direction=None,
features=None, features=None,
language=None, language=None,
@ -641,6 +630,12 @@ class FreeTypeFont:
.. versionadded:: 1.1.5 .. versionadded:: 1.1.5
:param fill: Optional fill function. By default, an internal Pillow function
will be used.
Deprecated. This parameter will be removed in Pillow 10
(2023-07-01).
:param direction: Direction of the text. It can be 'rtl' (right to :param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right) or 'ttb' (top to bottom). left), 'ltr' (left to right) or 'ttb' (top to bottom).
Requires libraqm. Requires libraqm.
@ -688,6 +683,10 @@ class FreeTypeFont:
:py:mod:`PIL.Image.core` interface module, and the text offset, the :py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking gap between the starting coordinate and the first marking
""" """
if fill is _UNSPECIFIED:
fill = Image.core.fill
else:
deprecate("fill", 10)
size, offset = self.font.getsize( size, offset = self.font.getsize(
text, mode, direction, features, language, anchor text, mode, direction, features, language, anchor
) )
@ -877,7 +876,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
try: try:
return freetype(font) return freetype(font)
except OSError: except OSError:
if not isPath(font): if not is_path(font):
raise raise
ttf_filename = os.path.basename(font) ttf_filename = os.path.basename(font)
@ -931,7 +930,7 @@ def load_path(filename):
:exception OSError: If the file could not be read. :exception OSError: If the file could not be read.
""" """
for directory in sys.path: for directory in sys.path:
if isDirectory(directory): if is_directory(directory):
if not isinstance(filename, str): if not isinstance(filename, str):
filename = filename.decode("utf-8") filename = filename.decode("utf-8")
try: try:

View File

@ -17,9 +17,9 @@
# #
import array import array
import warnings
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
from ._deprecate import deprecate
class ImagePalette: class ImagePalette:
@ -40,11 +40,7 @@ class ImagePalette:
self.palette = palette or bytearray() self.palette = palette or bytearray()
self.dirty = None self.dirty = None
if size != 0: if size != 0:
warnings.warn( deprecate("The size parameter", 10, None)
"The size parameter is deprecated and will be removed in Pillow 10 "
"(2023-07-01).",
DeprecationWarning,
)
if size != len(self.palette): if size != len(self.palette):
raise ValueError("wrong palette size") raise ValueError("wrong palette size")

View File

@ -20,7 +20,8 @@ import sys
from io import BytesIO from io import BytesIO
from . import Image from . import Image
from ._util import isPath from ._deprecate import deprecate
from ._util import is_path
qt_versions = [ qt_versions = [
["6", "PyQt6"], ["6", "PyQt6"],
@ -42,9 +43,13 @@ for qt_version, qt_module in qt_versions:
elif qt_module == "PyQt5": elif qt_module == "PyQt5":
from PyQt5.QtCore import QBuffer, QIODevice from PyQt5.QtCore import QBuffer, QIODevice
from PyQt5.QtGui import QImage, QPixmap, qRgba from PyQt5.QtGui import QImage, QPixmap, qRgba
deprecate("Support for PyQt5", 10, "PyQt6 or PySide6")
elif qt_module == "PySide2": elif qt_module == "PySide2":
from PySide2.QtCore import QBuffer, QIODevice from PySide2.QtCore import QBuffer, QIODevice
from PySide2.QtGui import QImage, QPixmap, qRgba from PySide2.QtGui import QImage, QPixmap, qRgba
deprecate("Support for PySide2", 10, "PyQt6 or PySide6")
except (ImportError, RuntimeError): except (ImportError, RuntimeError):
continue continue
qt_is_installed = True qt_is_installed = True
@ -140,7 +145,7 @@ def _toqclass_helper(im):
if hasattr(im, "toUtf8"): if hasattr(im, "toUtf8"):
# FIXME - is this really the best way to do this? # FIXME - is this really the best way to do this?
im = str(im.toUtf8(), "utf-8") im = str(im.toUtf8(), "utf-8")
if isPath(im): if is_path(im):
im = Image.open(im) im = Image.open(im)
exclusive_fp = True exclusive_fp = True

View File

@ -15,11 +15,12 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import warnings
from shlex import quote from shlex import quote
from PIL import Image from PIL import Image
from ._deprecate import deprecate
_viewers = [] _viewers = []
@ -120,11 +121,7 @@ class Viewer:
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -144,7 +141,7 @@ class WindowsViewer(Viewer):
def get_command(self, file, **options): def get_command(self, file, **options):
return ( return (
f'start "Pillow" /WAIT "{file}" ' f'start "Pillow" /WAIT "{file}" '
"&& ping -n 2 127.0.0.1 >NUL " "&& ping -n 4 127.0.0.1 >NUL "
f'&& del /f "{file}"' f'&& del /f "{file}"'
) )
@ -176,11 +173,7 @@ class MacViewer(Viewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -228,11 +221,7 @@ class XDGViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -261,11 +250,7 @@ class DisplayViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -296,11 +281,7 @@ class GmDisplayViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -325,11 +306,7 @@ class EogViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
@ -360,11 +337,7 @@ class XVViewer(UnixViewer):
""" """
if path is None: if path is None:
if "file" in options: if "file" in options:
warnings.warn( deprecate("The 'file' argument", 10, "'path'")
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")

View File

@ -78,10 +78,10 @@ class Stat:
v = [] v = []
for i in range(0, len(self.h), 256): for i in range(0, len(self.h), 256):
layerSum = 0.0 layer_sum = 0.0
for j in range(256): for j in range(256):
layerSum += j * self.h[i + j] layer_sum += j * self.h[i + j]
v.append(layerSum) v.append(layer_sum)
return v return v
def _getsum2(self): def _getsum2(self):

View File

@ -26,10 +26,10 @@
# #
import tkinter import tkinter
import warnings
from io import BytesIO from io import BytesIO
from . import Image from . import Image
from ._deprecate import deprecate
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Check for Tkinter interface hooks # Check for Tkinter interface hooks
@ -187,11 +187,7 @@ class PhotoImage:
""" """
if box is not None: if box is not None:
warnings.warn( deprecate("The box parameter", 10, None)
"The box parameter is deprecated and will be removed in Pillow 10 "
"(2023-07-01).",
DeprecationWarning,
)
# convert to blittable # convert to blittable
im.load() im.load()

View File

@ -45,6 +45,7 @@ from . import Image, ImageFile, TiffImagePlugin
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._deprecate import deprecate
from .JpegPresets import presets from .JpegPresets import presets
# #
@ -603,11 +604,7 @@ samplings = {
def convert_dict_qtables(qtables): def convert_dict_qtables(qtables):
warnings.warn( deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed")
"convert_dict_qtables is deprecated and will be removed in Pillow 10"
"(2023-07-01). Conversion is no longer needed.",
DeprecationWarning,
)
return qtables return qtables

View File

@ -193,15 +193,15 @@ class PcfFontFile(FontFile.FontFile):
for i in range(nbitmaps): for i in range(nbitmaps):
offsets.append(i32(fp.read(4))) offsets.append(i32(fp.read(4)))
bitmapSizes = [] bitmap_sizes = []
for i in range(4): for i in range(4):
bitmapSizes.append(i32(fp.read(4))) bitmap_sizes.append(i32(fp.read(4)))
# byteorder = format & 4 # non-zero => MSB # byteorder = format & 4 # non-zero => MSB
bitorder = format & 8 # non-zero => MSB bitorder = format & 8 # non-zero => MSB
padindex = format & 3 padindex = format & 3
bitmapsize = bitmapSizes[padindex] bitmapsize = bitmap_sizes[padindex]
offsets.append(bitmapsize) offsets.append(bitmapsize)
data = fp.read(bitmapsize) data = fp.read(bitmapsize)
@ -225,22 +225,22 @@ class PcfFontFile(FontFile.FontFile):
fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS)
firstCol, lastCol = i16(fp.read(2)), i16(fp.read(2)) first_col, last_col = i16(fp.read(2)), i16(fp.read(2))
firstRow, lastRow = i16(fp.read(2)), i16(fp.read(2)) first_row, last_row = i16(fp.read(2)), i16(fp.read(2))
i16(fp.read(2)) # default i16(fp.read(2)) # default
nencoding = (lastCol - firstCol + 1) * (lastRow - firstRow + 1) nencoding = (last_col - first_col + 1) * (last_row - first_row + 1)
encodingOffsets = [i16(fp.read(2)) for _ in range(nencoding)] encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)]
for i in range(firstCol, len(encoding)): for i in range(first_col, len(encoding)):
try: try:
encodingOffset = encodingOffsets[ encoding_offset = encoding_offsets[
ord(bytearray([i]).decode(self.charset_encoding)) ord(bytearray([i]).decode(self.charset_encoding))
] ]
if encodingOffset != 0xFFFF: if encoding_offset != 0xFFFF:
encoding[i] = encodingOffset encoding[i] = encoding_offset
except UnicodeDecodeError: except UnicodeDecodeError:
# character is not supported in selected encoding # character is not supported in selected encoding
pass pass

View File

@ -87,21 +87,21 @@ def _save(im, fp, filename, save_all=False):
for append_im in append_images: for append_im in append_images:
append_im.encoderinfo = im.encoderinfo.copy() append_im.encoderinfo = im.encoderinfo.copy()
ims.append(append_im) ims.append(append_im)
numberOfPages = 0 number_of_pages = 0
image_refs = [] image_refs = []
page_refs = [] page_refs = []
contents_refs = [] contents_refs = []
for im in ims: for im in ims:
im_numberOfPages = 1 im_number_of_pages = 1
if save_all: if save_all:
try: try:
im_numberOfPages = im.n_frames im_number_of_pages = im.n_frames
except AttributeError: except AttributeError:
# Image format does not have n_frames. # Image format does not have n_frames.
# It is a single frame image # It is a single frame image
pass pass
numberOfPages += im_numberOfPages number_of_pages += im_number_of_pages
for i in range(im_numberOfPages): for i in range(im_number_of_pages):
image_refs.append(existing_pdf.next_object_id(0)) image_refs.append(existing_pdf.next_object_id(0))
page_refs.append(existing_pdf.next_object_id(0)) page_refs.append(existing_pdf.next_object_id(0))
contents_refs.append(existing_pdf.next_object_id(0)) contents_refs.append(existing_pdf.next_object_id(0))
@ -111,9 +111,9 @@ def _save(im, fp, filename, save_all=False):
# catalog and list of pages # catalog and list of pages
existing_pdf.write_catalog() existing_pdf.write_catalog()
pageNumber = 0 page_number = 0
for imSequence in ims: for im_sequence in ims:
im_pages = ImageSequence.Iterator(imSequence) if save_all else [imSequence] im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
for im in im_pages: for im in im_pages:
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
# (packbits) or LZWDecode (tiff/lzw compression). Note that # (packbits) or LZWDecode (tiff/lzw compression). Note that
@ -176,7 +176,7 @@ def _save(im, fp, filename, save_all=False):
width, height = im.size width, height = im.size
existing_pdf.write_obj( existing_pdf.write_obj(
image_refs[pageNumber], image_refs[page_number],
stream=op.getvalue(), stream=op.getvalue(),
Type=PdfParser.PdfName("XObject"), Type=PdfParser.PdfName("XObject"),
Subtype=PdfParser.PdfName("Image"), Subtype=PdfParser.PdfName("Image"),
@ -193,10 +193,10 @@ def _save(im, fp, filename, save_all=False):
# page # page
existing_pdf.write_page( existing_pdf.write_page(
page_refs[pageNumber], page_refs[page_number],
Resources=PdfParser.PdfDict( Resources=PdfParser.PdfDict(
ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
XObject=PdfParser.PdfDict(image=image_refs[pageNumber]), XObject=PdfParser.PdfDict(image=image_refs[page_number]),
), ),
MediaBox=[ MediaBox=[
0, 0,
@ -204,7 +204,7 @@ def _save(im, fp, filename, save_all=False):
width * 72.0 / resolution, width * 72.0 / resolution,
height * 72.0 / resolution, height * 72.0 / resolution,
], ],
Contents=contents_refs[pageNumber], Contents=contents_refs[page_number],
) )
# #
@ -215,9 +215,9 @@ def _save(im, fp, filename, save_all=False):
height * 72.0 / resolution, height * 72.0 / resolution,
) )
existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
pageNumber += 1 page_number += 1
# #
# trailer # trailer

View File

@ -45,6 +45,7 @@ from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._binary import o32be as o32 from ._binary import o32be as o32
from ._deprecate import deprecate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -131,24 +132,11 @@ class Blend(IntEnum):
def __getattr__(name): def __getattr__(name):
deprecated = "deprecated and will be removed in Pillow 10 (2023-07-01). "
for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items():
if name.startswith(prefix): if name.startswith(prefix):
name = name[len(prefix) :] name = name[len(prefix) :]
if name in enum.__members__: if name in enum.__members__:
warnings.warn( deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
prefix
+ name
+ " is "
+ deprecated
+ "Use "
+ enum.__name__
+ "."
+ name
+ " instead.",
DeprecationWarning,
stacklevel=2,
)
return enum[name] return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@ -39,9 +39,9 @@ try:
except ImportError as ex: except ImportError as ex:
# Allow error import for doc purposes, but error out when accessing # Allow error import for doc purposes, but error out when accessing
# anything in core. # anything in core.
from ._util import deferred_error from ._util import DeferredError
FFI = ffi = deferred_error(ex) FFI = ffi = DeferredError(ex)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -138,7 +138,7 @@ def _save(im, fp, filename):
# Flip the image, since the origin of SGI file is the bottom-left corner # Flip the image, since the origin of SGI file is the bottom-left corner
orientation = -1 orientation = -1
# Define the file as SGI File Format # Define the file as SGI File Format
magicNumber = 474 magic_number = 474
# Run-Length Encoding Compression - Unsupported at this time # Run-Length Encoding Compression - Unsupported at this time
rle = 0 rle = 0
@ -167,11 +167,11 @@ def _save(im, fp, filename):
# Maximum Byte value (255 = 8bits per pixel) # Maximum Byte value (255 = 8bits per pixel)
pinmax = 255 pinmax = 255
# Image name (79 characters max, truncated below in write) # Image name (79 characters max, truncated below in write)
imgName = os.path.splitext(os.path.basename(filename))[0] img_name = os.path.splitext(os.path.basename(filename))[0]
imgName = imgName.encode("ascii", "ignore") img_name = img_name.encode("ascii", "ignore")
# Standard representation of pixel in the file # Standard representation of pixel in the file
colormap = 0 colormap = 0
fp.write(struct.pack(">h", magicNumber)) fp.write(struct.pack(">h", magic_number))
fp.write(o8(rle)) fp.write(o8(rle))
fp.write(o8(bpc)) fp.write(o8(bpc))
fp.write(struct.pack(">H", dim)) fp.write(struct.pack(">H", dim))
@ -181,8 +181,8 @@ def _save(im, fp, filename):
fp.write(struct.pack(">l", pinmin)) fp.write(struct.pack(">l", pinmin))
fp.write(struct.pack(">l", pinmax)) fp.write(struct.pack(">l", pinmax))
fp.write(struct.pack("4s", b"")) # dummy fp.write(struct.pack("4s", b"")) # dummy
fp.write(struct.pack("79s", imgName)) # truncates to 79 chars fp.write(struct.pack("79s", img_name)) # truncates to 79 chars
fp.write(struct.pack("s", b"")) # force null byte after imgname fp.write(struct.pack("s", b"")) # force null byte after img_name
fp.write(struct.pack(">l", colormap)) fp.write(struct.pack(">l", colormap))
fp.write(struct.pack("404s", b"")) # dummy fp.write(struct.pack("404s", b"")) # dummy

View File

@ -1336,14 +1336,14 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug(f"- size: {self.size}") logger.debug(f"- size: {self.size}")
sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,)) sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
if len(sampleFormat) > 1 and max(sampleFormat) == min(sampleFormat) == 1: if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
# SAMPLEFORMAT is properly per band, so an RGB image will # SAMPLEFORMAT is properly per band, so an RGB image will
# be (1,1,1). But, we don't support per band pixel types, # be (1,1,1). But, we don't support per band pixel types,
# and anything more than one band is a uint8. So, just # and anything more than one band is a uint8. So, just
# take the first element. Revisit this if adding support # take the first element. Revisit this if adding support
# for more exotic images. # for more exotic images.
sampleFormat = (1,) sample_format = (1,)
bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
@ -1364,18 +1364,18 @@ class TiffImageFile(ImageFile.ImageFile):
# presume it is the same number of bits for all of the samples. # presume it is the same number of bits for all of the samples.
bps_tuple = bps_tuple * bps_count bps_tuple = bps_tuple * bps_count
samplesPerPixel = self.tag_v2.get( samples_per_pixel = self.tag_v2.get(
SAMPLESPERPIXEL, SAMPLESPERPIXEL,
3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
) )
if len(bps_tuple) != samplesPerPixel: if len(bps_tuple) != samples_per_pixel:
raise SyntaxError("unknown data organization") raise SyntaxError("unknown data organization")
# mode: check photometric interpretation and bits per pixel # mode: check photometric interpretation and bits per pixel
key = ( key = (
self.tag_v2.prefix, self.tag_v2.prefix,
photo, photo,
sampleFormat, sample_format,
fillorder, fillorder,
bps_tuple, bps_tuple,
extra_tuple, extra_tuple,
@ -1880,16 +1880,16 @@ class AppendingTiffWriter:
self.whereToWriteNewIFDOffset = None self.whereToWriteNewIFDOffset = None
self.offsetOfNewPage = 0 self.offsetOfNewPage = 0
self.IIMM = IIMM = self.f.read(4) self.IIMM = iimm = self.f.read(4)
if not IIMM: if not iimm:
# empty file - first page # empty file - first page
self.isFirst = True self.isFirst = True
return return
self.isFirst = False self.isFirst = False
if IIMM == b"II\x2a\x00": if iimm == b"II\x2a\x00":
self.setEndian("<") self.setEndian("<")
elif IIMM == b"MM\x00\x2a": elif iimm == b"MM\x00\x2a":
self.setEndian(">") self.setEndian(">")
else: else:
raise RuntimeError("Invalid TIFF file header") raise RuntimeError("Invalid TIFF file header")
@ -1904,20 +1904,20 @@ class AppendingTiffWriter:
# fix offsets # fix offsets
self.f.seek(self.offsetOfNewPage) self.f.seek(self.offsetOfNewPage)
IIMM = self.f.read(4) iimm = self.f.read(4)
if not IIMM: if not iimm:
# raise RuntimeError("nothing written into new page") # raise RuntimeError("nothing written into new page")
# Make it easy to finish a frame without committing to a new one. # Make it easy to finish a frame without committing to a new one.
return return
if IIMM != self.IIMM: if iimm != self.IIMM:
raise RuntimeError("IIMM of new page doesn't match IIMM of first page") raise RuntimeError("IIMM of new page doesn't match IIMM of first page")
IFDoffset = self.readLong() ifd_offset = self.readLong()
IFDoffset += self.offsetOfNewPage ifd_offset += self.offsetOfNewPage
self.f.seek(self.whereToWriteNewIFDOffset) self.f.seek(self.whereToWriteNewIFDOffset)
self.writeLong(IFDoffset) self.writeLong(ifd_offset)
self.f.seek(IFDoffset) self.f.seek(ifd_offset)
self.fixIFD() self.fixIFD()
def newFrame(self): def newFrame(self):
@ -1948,9 +1948,9 @@ class AppendingTiffWriter:
pos = self.f.tell() pos = self.f.tell()
# pad to 16 byte boundary # pad to 16 byte boundary
padBytes = 16 - pos % 16 pad_bytes = 16 - pos % 16
if 0 < padBytes < 16: if 0 < pad_bytes < 16:
self.f.write(bytes(padBytes)) self.f.write(bytes(pad_bytes))
self.offsetOfNewPage = self.f.tell() self.offsetOfNewPage = self.f.tell()
def setEndian(self, endian): def setEndian(self, endian):
@ -1961,14 +1961,14 @@ class AppendingTiffWriter:
def skipIFDs(self): def skipIFDs(self):
while True: while True:
IFDoffset = self.readLong() ifd_offset = self.readLong()
if IFDoffset == 0: if ifd_offset == 0:
self.whereToWriteNewIFDOffset = self.f.tell() - 4 self.whereToWriteNewIFDOffset = self.f.tell() - 4
break break
self.f.seek(IFDoffset) self.f.seek(ifd_offset)
numTags = self.readShort() num_tags = self.readShort()
self.f.seek(numTags * 12, os.SEEK_CUR) self.f.seek(num_tags * 12, os.SEEK_CUR)
def write(self, data): def write(self, data):
return self.f.write(data) return self.f.write(data)
@ -1983,68 +1983,68 @@ class AppendingTiffWriter:
def rewriteLastShortToLong(self, value): def rewriteLastShortToLong(self, value):
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytesWritten = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytesWritten is not None and bytesWritten != 4: if bytes_written is not None and bytes_written != 4:
raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
def rewriteLastShort(self, value): def rewriteLastShort(self, value):
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytesWritten is not None and bytesWritten != 2: if bytes_written is not None and bytes_written != 2:
raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 2") raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")
def rewriteLastLong(self, value): def rewriteLastLong(self, value):
self.f.seek(-4, os.SEEK_CUR) self.f.seek(-4, os.SEEK_CUR)
bytesWritten = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytesWritten is not None and bytesWritten != 4: if bytes_written is not None and bytes_written != 4:
raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
def writeShort(self, value): def writeShort(self, value):
bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytesWritten is not None and bytesWritten != 2: if bytes_written is not None and bytes_written != 2:
raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 2") raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")
def writeLong(self, value): def writeLong(self, value):
bytesWritten = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytesWritten is not None and bytesWritten != 4: if bytes_written is not None and bytes_written != 4:
raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
def close(self): def close(self):
self.finalize() self.finalize()
self.f.close() self.f.close()
def fixIFD(self): def fixIFD(self):
numTags = self.readShort() num_tags = self.readShort()
for i in range(numTags): for i in range(num_tags):
tag, fieldType, count = struct.unpack(self.tagFormat, self.f.read(8)) tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))
fieldSize = self.fieldSizes[fieldType] field_size = self.fieldSizes[field_type]
totalSize = fieldSize * count total_size = field_size * count
isLocal = totalSize <= 4 is_local = total_size <= 4
if not isLocal: if not is_local:
offset = self.readLong() offset = self.readLong()
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
if tag in self.Tags: if tag in self.Tags:
curPos = self.f.tell() cur_pos = self.f.tell()
if isLocal: if is_local:
self.fixOffsets( self.fixOffsets(
count, isShort=(fieldSize == 2), isLong=(fieldSize == 4) count, isShort=(field_size == 2), isLong=(field_size == 4)
) )
self.f.seek(curPos + 4) self.f.seek(cur_pos + 4)
else: else:
self.f.seek(offset) self.f.seek(offset)
self.fixOffsets( self.fixOffsets(
count, isShort=(fieldSize == 2), isLong=(fieldSize == 4) count, isShort=(field_size == 2), isLong=(field_size == 4)
) )
self.f.seek(curPos) self.f.seek(cur_pos)
offset = curPos = None offset = cur_pos = None
elif isLocal: elif is_local:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)

66
src/PIL/_deprecate.py Normal file
View File

@ -0,0 +1,66 @@
from __future__ import annotations
import warnings
from . import __version__
def deprecate(
deprecated: str,
when: int | None,
replacement: str | None = None,
*,
action: str | None = None,
plural: bool = False,
) -> None:
"""
Deprecations helper.
:param deprecated: Name of thing to be deprecated.
:param when: Pillow major version to be removed in.
:param replacement: Name of replacement.
:param action: Instead of "replacement", give a custom call to action
e.g. "Upgrade to new thing".
:param plural: if the deprecated thing is plural, needing "are" instead of "is".
Usually of the form:
"[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd).
Use [replacement] instead."
You can leave out the replacement sentence:
"[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)"
Or with another call to action:
"[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd).
[action]."
"""
is_ = "are" if plural else "is"
if when is None:
removed = "a future version"
elif when <= int(__version__.split(".")[0]):
raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.")
elif when == 10:
removed = "Pillow 10 (2023-07-01)"
else:
raise ValueError(f"Unknown removal version, update {__name__}?")
if replacement and action:
raise ValueError("Use only one of 'replacement' and 'action'")
if replacement:
action = f". Use {replacement} instead."
elif action:
action = f". {action.rstrip('.')}."
else:
action = ""
warnings.warn(
f"{deprecated} {is_} deprecated and will be removed in {removed}{action}",
DeprecationWarning,
stacklevel=3,
)

View File

@ -2,9 +2,10 @@
""" """
import sys import sys
import tkinter import tkinter
import warnings
from tkinter import _tkinter as tk from tkinter import _tkinter as tk
from ._deprecate import deprecate
try: try:
if hasattr(sys, "pypy_find_executable"): if hasattr(sys, "pypy_find_executable"):
TKINTER_LIB = tk.tklib_cffi.__file__ TKINTER_LIB = tk.tklib_cffi.__file__
@ -17,9 +18,6 @@ except AttributeError:
tk_version = str(tkinter.TkVersion) tk_version = str(tkinter.TkVersion)
if tk_version == "8.4": if tk_version == "8.4":
warnings.warn( deprecate(
"Support for Tk/Tcl 8.4 is deprecated and will be removed" "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer"
" in Pillow 10 (2023-07-01). Please upgrade to Tk/Tcl 8.5 "
"or newer.",
DeprecationWarning,
) )

View File

@ -2,16 +2,16 @@ import os
from pathlib import Path from pathlib import Path
def isPath(f): def is_path(f):
return isinstance(f, (bytes, str, Path)) return isinstance(f, (bytes, str, Path))
# Checks if an object is a string, and that it points to a directory. def is_directory(f):
def isDirectory(f): """Checks if an object is a string, and that it points to a directory."""
return isPath(f) and os.path.isdir(f) return is_path(f) and os.path.isdir(f)
class deferred_error: class DeferredError:
def __init__(self, ex): def __init__(self, ex):
self.ex = ex self.ex = ex

View File

@ -280,9 +280,9 @@ deps = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.2.0.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/4.2.1.zip",
"filename": "harfbuzz-4.2.0.zip", "filename": "harfbuzz-4.2.1.zip",
"dir": "harfbuzz-4.2.0", "dir": "harfbuzz-4.2.1",
"build": [ "build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"), cmd_nmake(target="clean"),
@ -292,9 +292,9 @@ deps = {
"libs": [r"*.lib"], "libs": [r"*.lib"],
}, },
"fribidi": { "fribidi": {
"url": "https://github.com/fribidi/fribidi/archive/v1.0.11.zip", "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip",
"filename": "fribidi-1.0.11.zip", "filename": "fribidi-1.0.12.zip",
"dir": "fribidi-1.0.11", "dir": "fribidi-1.0.12",
"build": [ "build": [
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
cmd_cmake(), cmd_cmake(),