Skip to content

Commit

Permalink
Merge branch 'main' into lab
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere authored Apr 25, 2024
2 parents 745eb23 + 5832288 commit e932d9e
Show file tree
Hide file tree
Showing 32 changed files with 362 additions and 206 deletions.
2 changes: 1 addition & 1 deletion .ci/requirements-mypy.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
mypy==1.9.0
mypy==1.10.0
2 changes: 1 addition & 1 deletion .github/workflows/test-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ jobs:
debian-11-bullseye-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-38-amd64,
fedora-39-amd64,
fedora-40-amd64,
gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ jobs:
- python-version: "3.10"
PYTHONOPTIMIZE: 2
# M1 only available for 3.10+
- os: "macos-latest"
- os: "macos-13"
python-version: "3.9"
- os: "macos-latest"
- os: "macos-13"
python-version: "3.8"
exclude:
- os: "macos-14"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
matrix:
include:
- name: "macOS x86_64"
os: macos-latest
os: macos-13
cibw_arch: x86_64
macosx_deployment_target: "10.10"
- name: "macOS arm64"
Expand Down
4 changes: 4 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ build:
os: ubuntu-22.04
tools:
python: "3"
jobs:
post_checkout:
- git remote add upstream https://github.com/python-pillow/Pillow.git # For forks
- git fetch upstream --tags

python:
install:
Expand Down
12 changes: 12 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ Changelog (Pillow)
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
[Cirras, radarhere]

Expand Down
45 changes: 38 additions & 7 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@
uploader = "github_actions"


modes = (
"1",
"L",
"LA",
"La",
"P",
"PA",
"F",
"I",
"I;16",
"I;16L",
"I;16B",
"I;16N",
"RGB",
"RGBA",
"RGBa",
"RGBX",
"BGR;15",
"BGR;16",
"BGR;24",
"CMYK",
"YCbCr",
"HSV",
"LAB",
)


def upload(a: Image.Image, b: Image.Image) -> str | None:
if uploader == "show":
# local img.show for errors.
Expand Down Expand Up @@ -273,13 +300,17 @@ def _cached_hopper(mode: str) -> Image.Image:
im = hopper("L")
else:
im = hopper()
try:
im = im.convert(mode)
except ImportError:
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
im = im.convert(mode)
else:
try:
im = im.convert(mode)
except ImportError:
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
return im


Expand Down
2 changes: 2 additions & 0 deletions Tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def test(name: str, function: Callable[[str], bool]) -> None:
else:
assert function(name) == version
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)

for module in features.modules:
Expand Down
105 changes: 56 additions & 49 deletions Tests/test_file_libtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,24 @@ def test_additional_metadata(

im.save(out, tiffinfo=new_ifd)

def test_custom_metadata(self, tmp_path: Path) -> None:
@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)

class Tc(NamedTuple):
value: Any
type: int
Expand Down Expand Up @@ -281,53 +298,43 @@ class Tc(NamedTuple):
)
}

libtiffs = [False]
if Image.core.libtiff_support_custom_tags:
libtiffs.append(True)

for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff

def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper()

out = str(tmp_path / "temp.tif")
im.save(out, tiffinfo=tiffinfo)

with Image.open(out) as reloaded:
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
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

assert reloaded_value == value

# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata.value
ifd.tagtype[tag] = tagdata.type
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 check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper()

out = str(tmp_path / "temp.tif")
im.save(out, tiffinfo=tiffinfo)

with Image.open(out) as reloaded:
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
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

assert reloaded_value == value

# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata.value
ifd.tagtype[tag] = tagdata.type
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
}
)

def test_osubfiletype(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
Expand Down Expand Up @@ -741,7 +748,7 @@ def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None:
pytest.param(
True,
marks=pytest.mark.skipif(
not Image.core.libtiff_support_custom_tags,
not getattr(Image.core, "libtiff_support_custom_tags", False),
reason="Custom tags not supported by older libtiff",
),
),
Expand Down
4 changes: 3 additions & 1 deletion Tests/test_file_png.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ def get_chunks(self, filename: str) -> list[bytes]:

def test_sanity(self, tmp_path: Path) -> None:
# 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")

Expand Down
65 changes: 25 additions & 40 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,45 +28,27 @@
assert_image_similar_tofile,
assert_not_all_same,
hopper,
is_big_endian,
is_win32,
mark_if_feature_version,
modes,
skip_unless_feature,
)

# name, pixel size
image_modes = (
("1", 1),
("L", 1),
("LA", 4),
("La", 4),
("P", 1),
("PA", 4),
("F", 4),
("I", 4),
("I;16", 2),
("I;16L", 2),
("I;16B", 2),
("I;16N", 2),
("RGB", 4),
("RGBA", 4),
("RGBa", 4),
("RGBX", 4),
("BGR;15", 2),
("BGR;16", 2),
("BGR;24", 3),
("CMYK", 4),
("YCbCr", 4),
("HSV", 4),
("LAB", 4),
)

image_mode_names = [name for name, _ in image_modes]
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
return Image.new(mode, size)
else:
return Image.new(mode, size)


class TestImage:
@pytest.mark.parametrize("mode", image_mode_names)
@pytest.mark.parametrize("mode", modes)
def test_image_modes_success(self, mode: str) -> None:
Image.new(mode, (1, 1))
helper_image_new(mode, (1, 1))

@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None:
Expand Down Expand Up @@ -1045,30 +1027,33 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None:


class TestImageBytes:
@pytest.mark.parametrize("mode", image_mode_names)
@pytest.mark.parametrize("mode", modes)
def test_roundtrip_bytes_constructor(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()

reloaded = Image.frombytes(mode, im.size, source_bytes)
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
reloaded = Image.frombytes(mode, im.size, source_bytes)
else:
reloaded = Image.frombytes(mode, im.size, source_bytes)
assert reloaded.tobytes() == source_bytes

@pytest.mark.parametrize("mode", image_mode_names)
@pytest.mark.parametrize("mode", modes)
def test_roundtrip_bytes_method(self, mode: str) -> None:
im = hopper(mode)
source_bytes = im.tobytes()

reloaded = Image.new(mode, im.size)
reloaded = helper_image_new(mode, im.size)
reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == source_bytes

@pytest.mark.parametrize(("mode", "pixelsize"), image_modes)
def test_getdata_putdata(self, mode: str, pixelsize: int) -> None:
im = Image.new(mode, (2, 2))
source_bytes = bytes(range(im.width * im.height * pixelsize))
im.frombytes(source_bytes)

reloaded = Image.new(mode, im.size)
@pytest.mark.parametrize("mode", modes)
def test_getdata_putdata(self, mode: str) -> None:
if is_big_endian and mode == "BGR;15":
pytest.xfail("Known failure of BGR;15 on big-endian")
im = hopper(mode)
reloaded = helper_image_new(mode, im.size)
reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded)

Expand Down
Loading

0 comments on commit e932d9e

Please sign in to comment.