mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 17:36:18 +03:00
Merge branch 'main' into bgr
This commit is contained in:
commit
49ce63d257
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -57,9 +57,9 @@ jobs:
|
||||||
- python-version: "3.10"
|
- python-version: "3.10"
|
||||||
PYTHONOPTIMIZE: 2
|
PYTHONOPTIMIZE: 2
|
||||||
# M1 only available for 3.10+
|
# M1 only available for 3.10+
|
||||||
- os: "macos-latest"
|
- os: "macos-13"
|
||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
- os: "macos-latest"
|
- os: "macos-13"
|
||||||
python-version: "3.8"
|
python-version: "3.8"
|
||||||
exclude:
|
exclude:
|
||||||
- os: "macos-14"
|
- os: "macos-14"
|
||||||
|
|
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
|
@ -97,7 +97,7 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: "macOS x86_64"
|
- name: "macOS x86_64"
|
||||||
os: macos-latest
|
os: macos-13
|
||||||
cibw_arch: x86_64
|
cibw_arch: x86_64
|
||||||
macosx_deployment_target: "10.10"
|
macosx_deployment_target: "10.10"
|
||||||
- name: "macOS arm64"
|
- name: "macOS arm64"
|
||||||
|
|
12
CHANGES.rst
12
CHANGES.rst
|
@ -5,6 +5,18 @@ Changelog (Pillow)
|
||||||
10.4.0 (unreleased)
|
10.4.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Support reading P mode TIFF images with padding #7996
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Deprecate support for libtiff < 4 #7998
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Corrected ImageShow UnixViewer command #7987
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Use functools.cached_property in ImageStat #7952
|
||||||
|
[nulano, hugovk, radarhere]
|
||||||
|
|
||||||
- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956
|
- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956
|
||||||
[Cirras, radarhere]
|
[Cirras, radarhere]
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ The core image library is designed for fast access to data stored in a few basic
|
||||||
## More Information
|
## More Information
|
||||||
|
|
||||||
- [Documentation](https://pillow.readthedocs.io/)
|
- [Documentation](https://pillow.readthedocs.io/)
|
||||||
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
|
- [Installation](https://pillow.readthedocs.io/en/latest/installation/basic-installation.html)
|
||||||
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
|
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
|
||||||
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
|
- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
|
||||||
- [Issues](https://github.com/python-pillow/Pillow/issues)
|
- [Issues](https://github.com/python-pillow/Pillow/issues)
|
||||||
|
|
|
@ -37,6 +37,8 @@ def test_version() -> None:
|
||||||
else:
|
else:
|
||||||
assert function(name) == version
|
assert function(name) == version
|
||||||
if name != "PIL":
|
if name != "PIL":
|
||||||
|
if name == "zlib" and version is not None:
|
||||||
|
version = version.replace(".zlib-ng", "")
|
||||||
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
assert version is None or re.search(r"\d+(\.\d+)*$", version)
|
||||||
|
|
||||||
for module in features.modules:
|
for module in features.modules:
|
||||||
|
|
|
@ -185,7 +185,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert field in reloaded, f"{field} not in metadata"
|
assert field in reloaded, f"{field} not in metadata"
|
||||||
|
|
||||||
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
|
@pytest.mark.valgrind_known_error(reason="Known invalid metadata")
|
||||||
def test_additional_metadata(self, tmp_path: Path) -> None:
|
def test_additional_metadata(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
# these should not crash. Seriously dummy data, most of it doesn't make
|
# these should not crash. Seriously dummy data, most of it doesn't make
|
||||||
# any sense, so we're running up against limits where we're asking
|
# any sense, so we're running up against limits where we're asking
|
||||||
# libtiff to do stupid things.
|
# libtiff to do stupid things.
|
||||||
|
@ -236,13 +238,28 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
del new_ifd[338]
|
del new_ifd[338]
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||||
|
|
||||||
im.save(out, tiffinfo=new_ifd)
|
im.save(out, tiffinfo=new_ifd)
|
||||||
|
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
@pytest.mark.parametrize(
|
||||||
|
"libtiff",
|
||||||
|
(
|
||||||
|
pytest.param(
|
||||||
|
True,
|
||||||
|
marks=pytest.mark.skipif(
|
||||||
|
not getattr(Image.core, "libtiff_support_custom_tags", False),
|
||||||
|
reason="Custom tags not supported by older libtiff",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_custom_metadata(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||||
|
|
||||||
def test_custom_metadata(self, tmp_path: Path) -> None:
|
|
||||||
class Tc(NamedTuple):
|
class Tc(NamedTuple):
|
||||||
value: Any
|
value: Any
|
||||||
type: int
|
type: int
|
||||||
|
@ -281,53 +298,43 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
libtiffs = [False]
|
def check_tags(
|
||||||
if Image.core.libtiff_support_custom_tags:
|
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||||
libtiffs.append(True)
|
) -> None:
|
||||||
|
im = hopper()
|
||||||
|
|
||||||
for libtiff in libtiffs:
|
out = str(tmp_path / "temp.tif")
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
im.save(out, tiffinfo=tiffinfo)
|
||||||
|
|
||||||
def check_tags(
|
with Image.open(out) as reloaded:
|
||||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
for tag, value in tiffinfo.items():
|
||||||
) -> None:
|
reloaded_value = reloaded.tag_v2[tag]
|
||||||
im = hopper()
|
if (
|
||||||
|
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
|
||||||
|
and libtiff
|
||||||
|
):
|
||||||
|
# libtiff does not support real RATIONALS
|
||||||
|
assert round(abs(float(reloaded_value) - float(value)), 7) == 0
|
||||||
|
continue
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tif")
|
assert reloaded_value == value
|
||||||
im.save(out, tiffinfo=tiffinfo)
|
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
# Test with types
|
||||||
for tag, value in tiffinfo.items():
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
reloaded_value = reloaded.tag_v2[tag]
|
for tag, tagdata in custom.items():
|
||||||
if (
|
ifd[tag] = tagdata.value
|
||||||
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
|
ifd.tagtype[tag] = tagdata.type
|
||||||
and libtiff
|
check_tags(ifd)
|
||||||
):
|
|
||||||
# libtiff does not support real RATIONALS
|
|
||||||
assert (
|
|
||||||
round(abs(float(reloaded_value) - float(value)), 7) == 0
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
assert reloaded_value == value
|
# Test without types. This only works for some types, int for example are
|
||||||
|
# always encoded as LONG and not SIGNED_LONG.
|
||||||
# Test with types
|
check_tags(
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
{
|
||||||
for tag, tagdata in custom.items():
|
tag: tagdata.value
|
||||||
ifd[tag] = tagdata.value
|
for tag, tagdata in custom.items()
|
||||||
ifd.tagtype[tag] = tagdata.type
|
if tagdata.supported_by_default
|
||||||
check_tags(ifd)
|
}
|
||||||
|
)
|
||||||
# Test without types. This only works for some types, int for example are
|
|
||||||
# always encoded as LONG and not SIGNED_LONG.
|
|
||||||
check_tags(
|
|
||||||
{
|
|
||||||
tag: tagdata.value
|
|
||||||
for tag, tagdata in custom.items()
|
|
||||||
if tagdata.supported_by_default
|
|
||||||
}
|
|
||||||
)
|
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
|
||||||
|
|
||||||
def test_osubfiletype(self, tmp_path: Path) -> None:
|
def test_osubfiletype(self, tmp_path: Path) -> None:
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
@ -343,24 +350,24 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
# Should not segfault
|
# Should not segfault
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
def test_xmlpacket_tag(self, tmp_path: Path) -> None:
|
def test_xmlpacket_tag(
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
|
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
if 700 in reloaded.tag_v2:
|
if 700 in reloaded.tag_v2:
|
||||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||||
|
|
||||||
def test_int_dpi(self, tmp_path: Path) -> None:
|
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||||
# issue #1765
|
# issue #1765
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||||
im.save(out, dpi=(72, 72))
|
im.save(out, dpi=(72, 72))
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert reloaded.info["dpi"] == (72.0, 72.0)
|
assert reloaded.info["dpi"] == (72.0, 72.0)
|
||||||
|
|
||||||
|
@ -422,13 +429,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert "temp.tif" == reread.tag_v2[269]
|
assert "temp.tif" == reread.tag_v2[269]
|
||||||
assert "temp.tif" == reread.tag[269][0]
|
assert "temp.tif" == reread.tag[269][0]
|
||||||
|
|
||||||
def test_12bit_rawmode(self) -> None:
|
def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Are we generating the same interpretation
|
"""Are we generating the same interpretation
|
||||||
of the image as Imagemagick is?"""
|
of the image as Imagemagick is?"""
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open("Tests/images/12bit.cropped.tif") as im:
|
with Image.open("Tests/images/12bit.cropped.tif") as im:
|
||||||
im.load()
|
im.load()
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False)
|
||||||
# to make the target --
|
# to make the target --
|
||||||
# convert 12bit.cropped.tif -depth 16 tmp.tif
|
# convert 12bit.cropped.tif -depth 16 tmp.tif
|
||||||
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
|
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
|
||||||
|
@ -514,12 +521,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert_image_equal_tofile(im, out)
|
assert_image_equal_tofile(im, out)
|
||||||
|
|
||||||
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||||
def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None:
|
def test_palette_save(
|
||||||
|
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
|
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||||
im.save(out)
|
im.save(out)
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
# colormap/palette tag
|
# colormap/palette tag
|
||||||
|
@ -548,9 +556,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
os.close(fn)
|
os.close(fn)
|
||||||
|
|
||||||
def test_multipage(self) -> None:
|
def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# issue #862
|
# issue #862
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
||||||
|
|
||||||
|
@ -569,11 +577,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert im.size == (20, 20)
|
assert im.size == (20, 20)
|
||||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
|
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
|
||||||
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
|
||||||
def test_multipage_nframes(self) -> None:
|
|
||||||
# issue #862
|
# issue #862
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
frames = im.n_frames
|
frames = im.n_frames
|
||||||
assert frames == 3
|
assert frames == 3
|
||||||
|
@ -582,10 +588,8 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
# Should not raise ValueError: I/O operation on closed file
|
# Should not raise ValueError: I/O operation on closed file
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
def test_multipage_seek_backwards(self) -> None:
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
im.load()
|
im.load()
|
||||||
|
@ -593,24 +597,21 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
||||||
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
def test__next(self) -> None:
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
|
||||||
with Image.open("Tests/images/hopper.tif") as im:
|
with Image.open("Tests/images/hopper.tif") as im:
|
||||||
assert not im.tag.next
|
assert not im.tag.next
|
||||||
im.load()
|
im.load()
|
||||||
assert not im.tag.next
|
assert not im.tag.next
|
||||||
|
|
||||||
def test_4bit(self) -> None:
|
def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
test_file = "Tests/images/hopper_gray_4bpp.tif"
|
test_file = "Tests/images/hopper_gray_4bpp.tif"
|
||||||
original = hopper("L")
|
original = hopper("L")
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
@ -650,12 +651,12 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
assert im2.mode == "L"
|
assert im2.mode == "L"
|
||||||
assert_image_equal(im, im2)
|
assert_image_equal(im, im2)
|
||||||
|
|
||||||
def test_save_bytesio(self) -> None:
|
def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# PR 1011
|
# PR 1011
|
||||||
# Test TIFF saving to io.BytesIO() object.
|
# Test TIFF saving to io.BytesIO() object.
|
||||||
|
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
|
|
||||||
# Generate test image
|
# Generate test image
|
||||||
pilim = hopper()
|
pilim = hopper()
|
||||||
|
@ -672,9 +673,6 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
save_bytesio("packbits")
|
save_bytesio("packbits")
|
||||||
save_bytesio("tiff_lzw")
|
save_bytesio("tiff_lzw")
|
||||||
|
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
|
||||||
|
|
||||||
def test_save_ycbcr(self, tmp_path: Path) -> None:
|
def test_save_ycbcr(self, tmp_path: Path) -> None:
|
||||||
im = hopper("YCbCr")
|
im = hopper("YCbCr")
|
||||||
outfile = str(tmp_path / "temp.tif")
|
outfile = str(tmp_path / "temp.tif")
|
||||||
|
@ -694,15 +692,16 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
if Image.core.libtiff_support_custom_tags:
|
if Image.core.libtiff_support_custom_tags:
|
||||||
assert reloaded.tag_v2[34665] == 125456
|
assert reloaded.tag_v2[34665] == 125456
|
||||||
|
|
||||||
def test_crashing_metadata(self, tmp_path: Path) -> None:
|
def test_crashing_metadata(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
# issue 1597
|
# issue 1597
|
||||||
with Image.open("Tests/images/rdf.tif") as im:
|
with Image.open("Tests/images/rdf.tif") as im:
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
|
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||||
# this shouldn't crash
|
# this shouldn't crash
|
||||||
im.save(out, format="TIFF")
|
im.save(out, format="TIFF")
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
|
||||||
|
|
||||||
def test_page_number_x_0(self, tmp_path: Path) -> None:
|
def test_page_number_x_0(self, tmp_path: Path) -> None:
|
||||||
# Issue 973
|
# Issue 973
|
||||||
|
@ -733,36 +732,41 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
# Should not raise PermissionError.
|
# Should not raise PermissionError.
|
||||||
os.remove(tmpfile)
|
os.remove(tmpfile)
|
||||||
|
|
||||||
def test_read_icc(self) -> None:
|
def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||||
icc = img.info.get("icc_profile")
|
icc = img.info.get("icc_profile")
|
||||||
assert icc is not None
|
assert icc is not None
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||||
icc_libtiff = img.info.get("icc_profile")
|
icc_libtiff = img.info.get("icc_profile")
|
||||||
assert icc_libtiff is not None
|
assert icc_libtiff is not None
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
|
||||||
assert icc == icc_libtiff
|
assert icc == icc_libtiff
|
||||||
|
|
||||||
def test_write_icc(self, tmp_path: Path) -> None:
|
@pytest.mark.parametrize(
|
||||||
def check_write(libtiff: bool) -> None:
|
"libtiff",
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
(
|
||||||
|
pytest.param(
|
||||||
|
True,
|
||||||
|
marks=pytest.mark.skipif(
|
||||||
|
not getattr(Image.core, "libtiff_support_custom_tags", False),
|
||||||
|
reason="Custom tags not supported by older libtiff",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_write_icc(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||||
icc_profile = img.info["icc_profile"]
|
icc_profile = img.info["icc_profile"]
|
||||||
|
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
img.save(out, icc_profile=icc_profile)
|
img.save(out, icc_profile=icc_profile)
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert icc_profile == reloaded.info["icc_profile"]
|
assert icc_profile == reloaded.info["icc_profile"]
|
||||||
|
|
||||||
libtiffs = []
|
|
||||||
if Image.core.libtiff_support_custom_tags:
|
|
||||||
libtiffs.append(True)
|
|
||||||
libtiffs.append(False)
|
|
||||||
|
|
||||||
for libtiff in libtiffs:
|
|
||||||
check_write(libtiff)
|
|
||||||
|
|
||||||
def test_multipage_compression(self) -> None:
|
def test_multipage_compression(self) -> None:
|
||||||
with Image.open("Tests/images/compression.tif") as im:
|
with Image.open("Tests/images/compression.tif") as im:
|
||||||
|
@ -840,12 +844,13 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
|
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
|
||||||
|
|
||||||
def test_sampleformat_write(self, tmp_path: Path) -> None:
|
def test_sampleformat_write(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
im = Image.new("F", (1, 1))
|
im = Image.new("F", (1, 1))
|
||||||
out = str(tmp_path / "temp.tif")
|
out = str(tmp_path / "temp.tif")
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||||
im.save(out)
|
im.save(out)
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = False
|
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert reloaded.mode == "F"
|
assert reloaded.mode == "F"
|
||||||
|
@ -1091,15 +1096,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
def test_realloc_overflow(self) -> None:
|
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
|
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
|
||||||
with pytest.raises(OSError) as e:
|
with pytest.raises(OSError) as e:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
# Assert that the error code is IMAGING_CODEC_MEMORY
|
# Assert that the error code is IMAGING_CODEC_MEMORY
|
||||||
assert str(e.value) == "-9"
|
assert str(e.value) == "-9"
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||||
|
|
|
@ -85,7 +85,9 @@ class TestFilePng:
|
||||||
|
|
||||||
def test_sanity(self, tmp_path: Path) -> None:
|
def test_sanity(self, tmp_path: Path) -> None:
|
||||||
# internal version number
|
# internal version number
|
||||||
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
|
assert re.search(
|
||||||
|
r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib")
|
||||||
|
)
|
||||||
|
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,16 @@ def test_fromarray() -> None:
|
||||||
Image.fromarray(wrapped)
|
Image.fromarray(wrapped)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fromarray_strides_without_tobytes() -> None:
|
||||||
|
class Wrapper:
|
||||||
|
def __init__(self, arr_params: dict[str, Any]) -> None:
|
||||||
|
self.__array_interface__ = arr_params
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
|
||||||
|
Image.fromarray(wrapped, "L")
|
||||||
|
|
||||||
|
|
||||||
def test_fromarray_palette() -> None:
|
def test_fromarray_palette() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
i = im.convert("L")
|
i = im.convert("L")
|
||||||
|
|
|
@ -47,7 +47,11 @@ def test_iterator_min_frame() -> None:
|
||||||
assert i[index] == next(i)
|
assert i[index] == next(i)
|
||||||
|
|
||||||
|
|
||||||
def _test_multipage_tiff() -> None:
|
@pytest.mark.parametrize(
|
||||||
|
"libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
|
||||||
|
)
|
||||||
|
def test_multipage_tiff(monkeypatch: pytest.MonkeyPatch, libtiff: bool) -> None:
|
||||||
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", libtiff)
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
for index, frame in enumerate(ImageSequence.Iterator(im)):
|
for index, frame in enumerate(ImageSequence.Iterator(im)):
|
||||||
frame.load()
|
frame.load()
|
||||||
|
@ -55,17 +59,6 @@ def _test_multipage_tiff() -> None:
|
||||||
frame.convert("RGB")
|
frame.convert("RGB")
|
||||||
|
|
||||||
|
|
||||||
def test_tiff() -> None:
|
|
||||||
_test_multipage_tiff()
|
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("libtiff")
|
|
||||||
def test_libtiff() -> None:
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = True
|
|
||||||
_test_multipage_tiff()
|
|
||||||
TiffImagePlugin.READ_LIBTIFF = False
|
|
||||||
|
|
||||||
|
|
||||||
def test_consecutive() -> None:
|
def test_consecutive() -> None:
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
first_frame = None
|
first_frame = None
|
||||||
|
|
|
@ -3,10 +3,12 @@ from __future__ import annotations
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PIL import Image, TiffImagePlugin, features
|
import pytest
|
||||||
|
|
||||||
|
from PIL import Image, TiffImagePlugin
|
||||||
from PIL.TiffImagePlugin import IFDRational
|
from PIL.TiffImagePlugin import IFDRational
|
||||||
|
|
||||||
from .helper import hopper
|
from .helper import hopper, skip_unless_feature
|
||||||
|
|
||||||
|
|
||||||
def _test_equal(num, denom, target) -> None:
|
def _test_equal(num, denom, target) -> None:
|
||||||
|
@ -52,18 +54,18 @@ def test_nonetype() -> None:
|
||||||
assert xres and yres
|
assert xres and yres
|
||||||
|
|
||||||
|
|
||||||
def test_ifd_rational_save(tmp_path: Path) -> None:
|
@pytest.mark.parametrize(
|
||||||
methods = [True]
|
"libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
|
||||||
if features.check("libtiff"):
|
)
|
||||||
methods.append(False)
|
def test_ifd_rational_save(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
|
||||||
|
) -> None:
|
||||||
|
im = hopper()
|
||||||
|
out = str(tmp_path / "temp.tiff")
|
||||||
|
res = IFDRational(301, 1)
|
||||||
|
|
||||||
for libtiff in methods:
|
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
|
||||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
im.save(out, dpi=(res, res), compression="raw")
|
||||||
|
|
||||||
im = hopper()
|
with Image.open(out) as reloaded:
|
||||||
out = str(tmp_path / "temp.tiff")
|
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
|
||||||
res = IFDRational(301, 1)
|
|
||||||
im.save(out, dpi=(res, res), compression="raw")
|
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
|
||||||
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ clean:
|
||||||
-rm -rf $(BUILDDIR)/*
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
install-sphinx:
|
install-sphinx:
|
||||||
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph
|
$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph
|
||||||
|
|
||||||
.PHONY: html
|
.PHONY: html
|
||||||
html:
|
html:
|
||||||
|
|
10
docs/conf.py
10
docs/conf.py
|
@ -22,7 +22,7 @@ import PIL
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
needs_sphinx = "2.4"
|
needs_sphinx = "7.3"
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
@ -34,7 +34,6 @@ extensions = [
|
||||||
"sphinx.ext.viewcode",
|
"sphinx.ext.viewcode",
|
||||||
"sphinx_copybutton",
|
"sphinx_copybutton",
|
||||||
"sphinx_inline_tabs",
|
"sphinx_inline_tabs",
|
||||||
"sphinx_removed_in",
|
|
||||||
"sphinxext.opengraph",
|
"sphinxext.opengraph",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -121,12 +120,7 @@ nitpicky = True
|
||||||
# generating warnings in “nitpicky mode”. Note that type should include the domain name
|
# generating warnings in “nitpicky mode”. Note that type should include the domain name
|
||||||
# if present. Example entries would be ('py:func', 'int') or
|
# if present. Example entries would be ('py:func', 'int') or
|
||||||
# ('envvar', 'LD_LIBRARY_PATH').
|
# ('envvar', 'LD_LIBRARY_PATH').
|
||||||
nitpick_ignore = [
|
# nitpick_ignore = []
|
||||||
# Sphinx does not understand typing.Literal[-1]
|
|
||||||
# Will be fixed in a future version.
|
|
||||||
# https://github.com/sphinx-doc/sphinx/pull/11904
|
|
||||||
("py:obj", "typing.Literal[-1, 1]"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
|
@ -107,6 +107,14 @@ BGR;15, BGR 16 and BGR;24
|
||||||
|
|
||||||
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
|
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
|
||||||
|
|
||||||
|
Support for LibTIFF earlier than 4
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. deprecated:: 10.4.0
|
||||||
|
|
||||||
|
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||||
|
Upgrade to a newer version of LibTIFF instead.
|
||||||
|
|
||||||
Removed features
|
Removed features
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,8 @@ Constructing images
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. autofunction:: new
|
.. autofunction:: new
|
||||||
|
.. autoclass:: SupportsArrayInterface
|
||||||
|
:show-inheritance:
|
||||||
.. autofunction:: fromarray
|
.. autofunction:: fromarray
|
||||||
.. autofunction:: frombytes
|
.. autofunction:: frombytes
|
||||||
.. autofunction:: frombuffer
|
.. autofunction:: frombuffer
|
||||||
|
|
|
@ -7,67 +7,6 @@
|
||||||
The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or
|
The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or
|
||||||
for a region of an image.
|
for a region of an image.
|
||||||
|
|
||||||
.. py:class:: Stat(image_or_list, mask=None)
|
.. autoclass:: Stat
|
||||||
|
:members:
|
||||||
Calculate statistics for the given image. If a mask is included,
|
:special-members: __init__
|
||||||
only the regions covered by that mask are included in the
|
|
||||||
statistics. You can also pass in a previously calculated histogram.
|
|
||||||
|
|
||||||
:param image: A PIL image, or a precalculated histogram.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
For a PIL image, calculations rely on the
|
|
||||||
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
|
|
||||||
grouped into 256 bins, even if the image has more than 8 bits per
|
|
||||||
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
|
|
||||||
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
|
|
||||||
of more than 255.
|
|
||||||
|
|
||||||
:param mask: An optional mask.
|
|
||||||
|
|
||||||
.. py:attribute:: extrema
|
|
||||||
|
|
||||||
Min/max values for each band in the image.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
|
|
||||||
simply returns the low and high bins used. This is correct for
|
|
||||||
images with 8 bits per channel, but fails for other modes such as
|
|
||||||
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
|
|
||||||
return per-band extrema for the image. This is more correct and
|
|
||||||
efficient because, for non-8-bit modes, the histogram method uses
|
|
||||||
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
|
|
||||||
|
|
||||||
.. py:attribute:: count
|
|
||||||
|
|
||||||
Total number of pixels for each band in the image.
|
|
||||||
|
|
||||||
.. py:attribute:: sum
|
|
||||||
|
|
||||||
Sum of all pixels for each band in the image.
|
|
||||||
|
|
||||||
.. py:attribute:: sum2
|
|
||||||
|
|
||||||
Squared sum of all pixels for each band in the image.
|
|
||||||
|
|
||||||
.. py:attribute:: mean
|
|
||||||
|
|
||||||
Average (arithmetic mean) pixel level for each band in the image.
|
|
||||||
|
|
||||||
.. py:attribute:: median
|
|
||||||
|
|
||||||
Median pixel level for each band in the image.
|
|
||||||
|
|
||||||
.. py:attribute:: rms
|
|
||||||
|
|
||||||
RMS (root-mean-square) for each band in the image.
|
|
||||||
|
|
||||||
.. py:attribute:: var
|
|
||||||
|
|
||||||
Variance for each band in the image.
|
|
||||||
|
|
||||||
.. py:attribute:: stddev
|
|
||||||
|
|
||||||
Standard deviation for each band in the image.
|
|
||||||
|
|
54
docs/releasenotes/10.4.0.rst
Normal file
54
docs/releasenotes/10.4.0.rst
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
10.4.0
|
||||||
|
------
|
||||||
|
|
||||||
|
Security
|
||||||
|
========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:cve:`YYYY-XXXXX`: TODO
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Backwards Incompatible Changes
|
||||||
|
==============================
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
Deprecations
|
||||||
|
============
|
||||||
|
|
||||||
|
Support for LibTIFF earlier than 4
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Support for LibTIFF earlier than version 4 has been deprecated.
|
||||||
|
Upgrade to a newer version of LibTIFF instead.
|
||||||
|
|
||||||
|
API Changes
|
||||||
|
===========
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
API Additions
|
||||||
|
=============
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
=============
|
||||||
|
|
||||||
|
TODO
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
TODO
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
10.4.0
|
||||||
10.3.0
|
10.3.0
|
||||||
10.2.0
|
10.2.0
|
||||||
10.1.0
|
10.1.0
|
||||||
|
|
|
@ -42,10 +42,9 @@ dynamic = [
|
||||||
docs = [
|
docs = [
|
||||||
"furo",
|
"furo",
|
||||||
"olefile",
|
"olefile",
|
||||||
"sphinx>=2.4",
|
"sphinx>=7.3",
|
||||||
"sphinx-copybutton",
|
"sphinx-copybutton",
|
||||||
"sphinx-inline-tabs",
|
"sphinx-inline-tabs",
|
||||||
"sphinx-removed-in",
|
|
||||||
"sphinxext-opengraph",
|
"sphinxext-opengraph",
|
||||||
]
|
]
|
||||||
fpx = [
|
fpx = [
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -1018,7 +1018,7 @@ The headers or library files could not be found for {str(err)},
|
||||||
a required dependency when compiling Pillow from source.
|
a required dependency when compiling Pillow from source.
|
||||||
|
|
||||||
Please see the install instructions at:
|
Please see the install instructions at:
|
||||||
https://pillow.readthedocs.io/en/latest/installation.html
|
https://pillow.readthedocs.io/en/latest/installation/basic-installation.html
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sys.stderr.write(msg)
|
sys.stderr.write(msg)
|
||||||
|
|
|
@ -41,7 +41,7 @@ import warnings
|
||||||
from collections.abc import Callable, MutableMapping
|
from collections.abc import Callable, MutableMapping
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import IO, TYPE_CHECKING, Any, Literal, cast
|
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, cast
|
||||||
|
|
||||||
# VERSION was removed in Pillow 6.0.0.
|
# VERSION was removed in Pillow 6.0.0.
|
||||||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||||
|
@ -3025,7 +3025,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args) -> Image:
|
||||||
return im
|
return im
|
||||||
|
|
||||||
|
|
||||||
def frombuffer(mode, size, data, decoder_name="raw", *args):
|
def frombuffer(mode, size, data, decoder_name="raw", *args) -> Image:
|
||||||
"""
|
"""
|
||||||
Creates an image memory referencing pixel data in a byte buffer.
|
Creates an image memory referencing pixel data in a byte buffer.
|
||||||
|
|
||||||
|
@ -3081,7 +3081,17 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
|
||||||
return frombytes(mode, size, data, decoder_name, args)
|
return frombytes(mode, size, data, decoder_name, args)
|
||||||
|
|
||||||
|
|
||||||
def fromarray(obj, mode=None):
|
class SupportsArrayInterface(Protocol):
|
||||||
|
"""
|
||||||
|
An object that has an ``__array_interface__`` dictionary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __array_interface__(self) -> dict[str, Any]:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
|
||||||
"""
|
"""
|
||||||
Creates an image memory from an object exporting the array interface
|
Creates an image memory from an object exporting the array interface
|
||||||
(using the buffer protocol)::
|
(using the buffer protocol)::
|
||||||
|
@ -3160,8 +3170,11 @@ def fromarray(obj, mode=None):
|
||||||
if strides is not None:
|
if strides is not None:
|
||||||
if hasattr(obj, "tobytes"):
|
if hasattr(obj, "tobytes"):
|
||||||
obj = obj.tobytes()
|
obj = obj.tobytes()
|
||||||
else:
|
elif hasattr(obj, "tostring"):
|
||||||
obj = obj.tostring()
|
obj = obj.tostring()
|
||||||
|
else:
|
||||||
|
msg = "'strides' requires either tobytes() or tostring()"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
|
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
|
||||||
|
|
||||||
|
|
|
@ -838,8 +838,8 @@ def getProfileName(profile: _CmsProfileCompatible) -> str:
|
||||||
|
|
||||||
if not (model or manufacturer):
|
if not (model or manufacturer):
|
||||||
return (profile.profile.profile_description or "") + "\n"
|
return (profile.profile.profile_description or "") + "\n"
|
||||||
if not manufacturer or len(model) > 30: # type: ignore[arg-type]
|
if not manufacturer or (model and len(model) > 30):
|
||||||
return model + "\n" # type: ignore[operator]
|
return f"{model}\n"
|
||||||
return f"{model} - {manufacturer}\n"
|
return f"{model} - {manufacturer}\n"
|
||||||
|
|
||||||
except (AttributeError, OSError, TypeError, ValueError) as v:
|
except (AttributeError, OSError, TypeError, ValueError) as v:
|
||||||
|
|
|
@ -199,7 +199,7 @@ class UnixViewer(Viewer):
|
||||||
|
|
||||||
def get_command(self, file: str, **options: Any) -> str:
|
def get_command(self, file: str, **options: Any) -> str:
|
||||||
command = self.get_command_ex(file, **options)[0]
|
command = self.get_command_ex(file, **options)[0]
|
||||||
return f"({command} {quote(file)}"
|
return f"{command} {quote(file)}"
|
||||||
|
|
||||||
|
|
||||||
class XDGViewer(UnixViewer):
|
class XDGViewer(UnixViewer):
|
||||||
|
|
|
@ -23,35 +23,58 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from . import Image
|
||||||
|
|
||||||
|
|
||||||
class Stat:
|
class Stat:
|
||||||
def __init__(self, image_or_list, mask=None):
|
def __init__(
|
||||||
try:
|
self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None
|
||||||
if mask:
|
) -> None:
|
||||||
self.h = image_or_list.histogram(mask)
|
"""
|
||||||
else:
|
Calculate statistics for the given image. If a mask is included,
|
||||||
self.h = image_or_list.histogram()
|
only the regions covered by that mask are included in the
|
||||||
except AttributeError:
|
statistics. You can also pass in a previously calculated histogram.
|
||||||
self.h = image_or_list # assume it to be a histogram list
|
|
||||||
if not isinstance(self.h, list):
|
:param image: A PIL image, or a precalculated histogram.
|
||||||
msg = "first argument must be image or list"
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
For a PIL image, calculations rely on the
|
||||||
|
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
|
||||||
|
grouped into 256 bins, even if the image has more than 8 bits per
|
||||||
|
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
|
||||||
|
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
|
||||||
|
of more than 255.
|
||||||
|
|
||||||
|
:param mask: An optional mask.
|
||||||
|
"""
|
||||||
|
if isinstance(image_or_list, Image.Image):
|
||||||
|
self.h = image_or_list.histogram(mask)
|
||||||
|
elif isinstance(image_or_list, list):
|
||||||
|
self.h = image_or_list
|
||||||
|
else:
|
||||||
|
msg = "first argument must be image or list" # type: ignore[unreachable]
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
self.bands = list(range(len(self.h) // 256))
|
self.bands = list(range(len(self.h) // 256))
|
||||||
|
|
||||||
def __getattr__(self, id):
|
@cached_property
|
||||||
"""Calculate missing attribute"""
|
def extrema(self) -> list[tuple[int, int]]:
|
||||||
if id[:4] == "_get":
|
"""
|
||||||
raise AttributeError(id)
|
Min/max values for each band in the image.
|
||||||
# calculate missing attribute
|
|
||||||
v = getattr(self, "_get" + id)()
|
|
||||||
setattr(self, id, v)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def _getextrema(self):
|
.. note::
|
||||||
"""Get min/max values for each band in the image"""
|
This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
|
||||||
|
simply returns the low and high bins used. This is correct for
|
||||||
|
images with 8 bits per channel, but fails for other modes such as
|
||||||
|
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
|
||||||
|
return per-band extrema for the image. This is more correct and
|
||||||
|
efficient because, for non-8-bit modes, the histogram method uses
|
||||||
|
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
|
||||||
|
"""
|
||||||
|
|
||||||
def minmax(histogram):
|
def minmax(histogram: list[int]) -> tuple[int, int]:
|
||||||
res_min, res_max = 255, 0
|
res_min, res_max = 255, 0
|
||||||
for i in range(256):
|
for i in range(256):
|
||||||
if histogram[i]:
|
if histogram[i]:
|
||||||
|
@ -65,12 +88,14 @@ class Stat:
|
||||||
|
|
||||||
return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
|
return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
|
||||||
|
|
||||||
def _getcount(self):
|
@cached_property
|
||||||
"""Get total number of pixels in each layer"""
|
def count(self) -> list[int]:
|
||||||
|
"""Total number of pixels for each band in the image."""
|
||||||
return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
|
return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
|
||||||
|
|
||||||
def _getsum(self):
|
@cached_property
|
||||||
"""Get sum of all pixels in each layer"""
|
def sum(self) -> list[float]:
|
||||||
|
"""Sum of all pixels for each band in the image."""
|
||||||
|
|
||||||
v = []
|
v = []
|
||||||
for i in range(0, len(self.h), 256):
|
for i in range(0, len(self.h), 256):
|
||||||
|
@ -80,8 +105,9 @@ class Stat:
|
||||||
v.append(layer_sum)
|
v.append(layer_sum)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def _getsum2(self):
|
@cached_property
|
||||||
"""Get squared sum of all pixels in each layer"""
|
def sum2(self) -> list[float]:
|
||||||
|
"""Squared sum of all pixels for each band in the image."""
|
||||||
|
|
||||||
v = []
|
v = []
|
||||||
for i in range(0, len(self.h), 256):
|
for i in range(0, len(self.h), 256):
|
||||||
|
@ -91,12 +117,14 @@ class Stat:
|
||||||
v.append(sum2)
|
v.append(sum2)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def _getmean(self):
|
@cached_property
|
||||||
"""Get average pixel level for each layer"""
|
def mean(self) -> list[float]:
|
||||||
|
"""Average (arithmetic mean) pixel level for each band in the image."""
|
||||||
return [self.sum[i] / self.count[i] for i in self.bands]
|
return [self.sum[i] / self.count[i] for i in self.bands]
|
||||||
|
|
||||||
def _getmedian(self):
|
@cached_property
|
||||||
"""Get median pixel level for each layer"""
|
def median(self) -> list[int]:
|
||||||
|
"""Median pixel level for each band in the image."""
|
||||||
|
|
||||||
v = []
|
v = []
|
||||||
for i in self.bands:
|
for i in self.bands:
|
||||||
|
@ -110,19 +138,22 @@ class Stat:
|
||||||
v.append(j)
|
v.append(j)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
def _getrms(self):
|
@cached_property
|
||||||
"""Get RMS for each layer"""
|
def rms(self) -> list[float]:
|
||||||
|
"""RMS (root-mean-square) for each band in the image."""
|
||||||
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
|
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
|
||||||
|
|
||||||
def _getvar(self):
|
@cached_property
|
||||||
"""Get variance for each layer"""
|
def var(self) -> list[float]:
|
||||||
|
"""Variance for each band in the image."""
|
||||||
return [
|
return [
|
||||||
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
|
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
|
||||||
for i in self.bands
|
for i in self.bands
|
||||||
]
|
]
|
||||||
|
|
||||||
def _getstddev(self):
|
@cached_property
|
||||||
"""Get standard deviation for each layer"""
|
def stddev(self) -> list[float]:
|
||||||
|
"""Standard deviation for each band in the image."""
|
||||||
return [math.sqrt(self.var[i]) for i in self.bands]
|
return [math.sqrt(self.var[i]) for i in self.bands]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
||||||
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 .TiffTags import TYPES
|
from .TiffTags import TYPES
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -244,6 +245,7 @@ OPEN_INFO = {
|
||||||
(MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
|
(MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
|
||||||
(II, 3, (1,), 1, (8,), ()): ("P", "P"),
|
(II, 3, (1,), 1, (8,), ()): ("P", "P"),
|
||||||
(MM, 3, (1,), 1, (8,), ()): ("P", "P"),
|
(MM, 3, (1,), 1, (8,), ()): ("P", "P"),
|
||||||
|
(II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
|
||||||
(II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
|
(II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
|
||||||
(MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
|
(MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
|
||||||
(II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
|
(II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
|
||||||
|
@ -276,6 +278,9 @@ PREFIXES = [
|
||||||
b"II\x2B\x00", # BigTIFF with little-endian byte order
|
b"II\x2B\x00", # BigTIFF with little-endian byte order
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if not getattr(Image.core, "libtiff_support_custom_tags", True):
|
||||||
|
deprecate("Support for LibTIFF earlier than version 4", 12)
|
||||||
|
|
||||||
|
|
||||||
def _accept(prefix: bytes) -> bool:
|
def _accept(prefix: bytes) -> bool:
|
||||||
return prefix[:4] in PREFIXES
|
return prefix[:4] in PREFIXES
|
||||||
|
|
|
@ -3737,7 +3737,7 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) {
|
||||||
self->image->image32,
|
self->image->image32,
|
||||||
"image",
|
"image",
|
||||||
self->image->image);
|
self->image->image);
|
||||||
};
|
}
|
||||||
|
|
||||||
static struct PyGetSetDef getsetters[] = {
|
static struct PyGetSetDef getsetters[] = {
|
||||||
{"mode", (getter)_getattr_mode},
|
{"mode", (getter)_getattr_mode},
|
||||||
|
|
|
@ -427,7 +427,6 @@ error:
|
||||||
|
|
||||||
PyObject *
|
PyObject *
|
||||||
PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
|
PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
|
||||||
int clip;
|
|
||||||
HANDLE handle = NULL;
|
HANDLE handle = NULL;
|
||||||
int size;
|
int size;
|
||||||
void *data;
|
void *data;
|
||||||
|
|
|
@ -254,9 +254,8 @@ static void
|
||||||
rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) {
|
rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) {
|
||||||
int x;
|
int x;
|
||||||
for (x = 0; x < xsize; x++, in += 4) {
|
for (x = 0; x < xsize; x++, in += 4) {
|
||||||
UINT8 v = CLIP16(L24(in) >> 16);
|
*out_++ = L24(in) >> 16;
|
||||||
*out_++ = v;
|
*out_++ = 0;
|
||||||
*out_++ = v >> 8;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,9 +263,8 @@ static void
|
||||||
rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) {
|
rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) {
|
||||||
int x;
|
int x;
|
||||||
for (x = 0; x < xsize; x++, in += 4) {
|
for (x = 0; x < xsize; x++, in += 4) {
|
||||||
UINT8 v = CLIP16(L24(in) >> 16);
|
*out_++ = 0;
|
||||||
*out_++ = v >> 8;
|
*out_++ = L24(in) >> 16;
|
||||||
*out_++ = v;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,11 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
|
||||||
ImagingSectionCookie cookie;
|
ImagingSectionCookie cookie;
|
||||||
|
|
||||||
/* Assume there's enough data in the buffer */
|
/* Assume there's enough data in the buffer */
|
||||||
if (!im) {
|
if (!im || im->bands != 3) {
|
||||||
return (Imaging)ImagingError_ModeError();
|
return (Imaging)ImagingError_ModeError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(mode, "L") == 0 && im->bands == 3) {
|
if (strcmp(mode, "L") == 0) {
|
||||||
imOut = ImagingNewDirty("L", im->xsize, im->ysize);
|
imOut = ImagingNewDirty("L", im->xsize, im->ysize);
|
||||||
if (!imOut) {
|
if (!imOut) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
@ -47,7 +47,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
|
||||||
}
|
}
|
||||||
ImagingSectionLeave(&cookie);
|
ImagingSectionLeave(&cookie);
|
||||||
|
|
||||||
} else if (strlen(mode) == 3 && im->bands == 3) {
|
} else if (strlen(mode) == 3) {
|
||||||
imOut = ImagingNewDirty(mode, im->xsize, im->ysize);
|
imOut = ImagingNewDirty(mode, im->xsize, im->ysize);
|
||||||
if (!imOut) {
|
if (!imOut) {
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
|
@ -1582,6 +1582,7 @@ static struct {
|
||||||
{"P", "P", 8, copy1},
|
{"P", "P", 8, copy1},
|
||||||
{"P", "P;R", 8, unpackLR},
|
{"P", "P;R", 8, unpackLR},
|
||||||
{"P", "L", 8, copy1},
|
{"P", "L", 8, copy1},
|
||||||
|
{"P", "PX", 16, unpackL16B},
|
||||||
|
|
||||||
/* palette w. alpha */
|
/* palette w. alpha */
|
||||||
{"PA", "PA", 16, unpackLA},
|
{"PA", "PA", 16, unpackLA},
|
||||||
|
|
Loading…
Reference in New Issue
Block a user