mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-10 19:56:47 +03:00
463 lines
14 KiB
Python
463 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from PIL import Image, TiffImagePlugin, TiffTags
|
|
from PIL.TiffImagePlugin import IFDRational
|
|
|
|
from .helper import assert_deep_equal, hopper
|
|
|
|
TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}
|
|
|
|
|
|
def test_rt_metadata(tmp_path: Path) -> None:
|
|
"""Test writing arbitrary metadata into the tiff image directory
|
|
Use case is ImageJ private tags, one numeric, one arbitrary
|
|
data. https://github.com/python-pillow/Pillow/issues/291
|
|
"""
|
|
|
|
img = hopper()
|
|
|
|
# Behaviour change: re #1416
|
|
# Pre ifd rewrite, ImageJMetaData was being written as a string(2),
|
|
# Post ifd rewrite, it's defined as arbitrary bytes(7). It should
|
|
# roundtrip with the actual bytes, rather than stripped text
|
|
# of the premerge tests.
|
|
#
|
|
# For text items, we still have to decode('ascii','replace') because
|
|
# the tiff file format can't take 8 bit bytes in that field.
|
|
|
|
base_text_data = "This is some arbitrary metadata for a text field"
|
|
bin_data = base_text_data.encode("ascii") + b" \xff"
|
|
text_data = base_text_data + " " + chr(255)
|
|
reloaded_text_data = base_text_data + " ?"
|
|
float_data = 12.345
|
|
double_data = 67.89
|
|
info = TiffImagePlugin.ImageFileDirectory()
|
|
|
|
ImageJMetaData = TAG_IDS["ImageJMetaData"]
|
|
ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
|
|
ImageDescription = TAG_IDS["ImageDescription"]
|
|
|
|
info[ImageJMetaDataByteCounts] = len(bin_data)
|
|
info[ImageJMetaData] = bin_data
|
|
info[TAG_IDS["RollAngle"]] = float_data
|
|
info.tagtype[TAG_IDS["RollAngle"]] = 11
|
|
info[TAG_IDS["YawAngle"]] = double_data
|
|
info.tagtype[TAG_IDS["YawAngle"]] = 12
|
|
|
|
info[ImageDescription] = text_data
|
|
|
|
f = str(tmp_path / "temp.tif")
|
|
|
|
img.save(f, tiffinfo=info)
|
|
|
|
with Image.open(f) as loaded:
|
|
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
|
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
|
|
|
assert loaded.tag[ImageJMetaData] == bin_data
|
|
assert loaded.tag_v2[ImageJMetaData] == bin_data
|
|
|
|
assert loaded.tag[ImageDescription] == (reloaded_text_data,)
|
|
assert loaded.tag_v2[ImageDescription] == reloaded_text_data
|
|
|
|
loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
|
|
assert round(abs(loaded_float - float_data), 5) == 0
|
|
loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
|
|
assert round(abs(loaded_double - double_data), 7) == 0
|
|
|
|
# check with 2 element ImageJMetaDataByteCounts, issue #2006
|
|
|
|
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
|
img.save(f, tiffinfo=info)
|
|
with Image.open(f) as loaded:
|
|
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
|
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
|
|
|
|
|
def test_read_metadata() -> None:
|
|
with Image.open("Tests/images/hopper_g4.tif") as img:
|
|
assert {
|
|
"YResolution": IFDRational(4294967295, 113653537),
|
|
"PlanarConfiguration": 1,
|
|
"BitsPerSample": (1,),
|
|
"ImageLength": 128,
|
|
"Compression": 4,
|
|
"FillOrder": 1,
|
|
"RowsPerStrip": 128,
|
|
"ResolutionUnit": 3,
|
|
"PhotometricInterpretation": 0,
|
|
"PageNumber": (0, 1),
|
|
"XResolution": IFDRational(4294967295, 113653537),
|
|
"ImageWidth": 128,
|
|
"Orientation": 1,
|
|
"StripByteCounts": (1968,),
|
|
"SamplesPerPixel": 1,
|
|
"StripOffsets": (8,),
|
|
} == img.tag_v2.named()
|
|
|
|
assert {
|
|
"YResolution": ((4294967295, 113653537),),
|
|
"PlanarConfiguration": (1,),
|
|
"BitsPerSample": (1,),
|
|
"ImageLength": (128,),
|
|
"Compression": (4,),
|
|
"FillOrder": (1,),
|
|
"RowsPerStrip": (128,),
|
|
"ResolutionUnit": (3,),
|
|
"PhotometricInterpretation": (0,),
|
|
"PageNumber": (0, 1),
|
|
"XResolution": ((4294967295, 113653537),),
|
|
"ImageWidth": (128,),
|
|
"Orientation": (1,),
|
|
"StripByteCounts": (1968,),
|
|
"SamplesPerPixel": (1,),
|
|
"StripOffsets": (8,),
|
|
} == img.tag.named()
|
|
|
|
|
|
def test_write_metadata(tmp_path: Path) -> None:
|
|
"""Test metadata writing through the python code"""
|
|
with Image.open("Tests/images/hopper.tif") as img:
|
|
f = str(tmp_path / "temp.tiff")
|
|
del img.tag[278]
|
|
img.save(f, tiffinfo=img.tag)
|
|
|
|
original = img.tag_v2.named()
|
|
|
|
with Image.open(f) as loaded:
|
|
reloaded = loaded.tag_v2.named()
|
|
|
|
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
|
|
|
|
for tag, value in reloaded.items():
|
|
if tag in ignored:
|
|
continue
|
|
if isinstance(original[tag], tuple) and isinstance(
|
|
original[tag][0], IFDRational
|
|
):
|
|
# Need to compare element by element in the tuple,
|
|
# not comparing tuples of object references
|
|
assert_deep_equal(
|
|
original[tag],
|
|
value,
|
|
f"{tag} didn't roundtrip, {original[tag]}, {value}",
|
|
)
|
|
else:
|
|
assert (
|
|
original[tag] == value
|
|
), f"{tag} didn't roundtrip, {original[tag]}, {value}"
|
|
|
|
for tag, value in original.items():
|
|
if tag not in ignored:
|
|
assert value == reloaded[tag], f"{tag} didn't roundtrip"
|
|
|
|
|
|
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
|
out = str(tmp_path / "temp.tiff")
|
|
with Image.open("Tests/images/hopper.tif") as im:
|
|
info = im.tag_v2
|
|
del info[278]
|
|
|
|
# Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT
|
|
im = im.resize((500, 500))
|
|
info[TiffImagePlugin.IMAGEWIDTH] = im.width
|
|
|
|
# STRIPBYTECOUNTS can be a SHORT or a LONG
|
|
info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT
|
|
|
|
im.save(out, tiffinfo=info)
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
|
|
|
|
|
|
def test_no_duplicate_50741_tag() -> None:
|
|
assert TAG_IDS["MakerNoteSafety"] == 50741
|
|
assert TAG_IDS["BestQualityScale"] == 50780
|
|
|
|
|
|
def test_iptc(tmp_path: Path) -> None:
|
|
out = str(tmp_path / "temp.tiff")
|
|
with Image.open("Tests/images/hopper.Lab.tif") as im:
|
|
im.save(out)
|
|
|
|
|
|
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
|
|
def test_writing_other_types_to_ascii(
|
|
value: bytes | int, expected: str, tmp_path: Path
|
|
) -> None:
|
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
|
|
|
tag = TiffTags.TAGS_V2[271]
|
|
assert tag.type == TiffTags.ASCII
|
|
|
|
info[271] = value
|
|
|
|
im = hopper()
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info)
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert reloaded.tag_v2[271] == expected
|
|
|
|
|
|
@pytest.mark.parametrize("value", (1, IFDRational(1)))
|
|
def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
|
|
im = hopper()
|
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
|
|
|
tag = TiffTags.TAGS_V2[700]
|
|
assert tag.type == TiffTags.BYTE
|
|
|
|
info[700] = value
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info)
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert reloaded.tag_v2[700] == b"\x01"
|
|
|
|
|
|
def test_writing_other_types_to_undefined(tmp_path: Path) -> None:
|
|
im = hopper()
|
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
|
|
|
tag = TiffTags.TAGS_V2[33723]
|
|
assert tag.type == TiffTags.UNDEFINED
|
|
|
|
info[33723] = 1
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info)
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert reloaded.tag_v2[33723] == b"1"
|
|
|
|
|
|
def test_undefined_zero(tmp_path: Path) -> None:
|
|
# Check that the tag has not been changed since this test was created
|
|
tag = TiffTags.TAGS_V2[45059]
|
|
assert tag.type == TiffTags.UNDEFINED
|
|
assert tag.length == 0
|
|
|
|
info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00")
|
|
info[45059] = b"test"
|
|
|
|
# Assert that the tag value does not change by setting it to itself
|
|
original = info[45059]
|
|
info[45059] = info[45059]
|
|
assert info[45059] == original
|
|
|
|
|
|
def test_empty_metadata() -> None:
|
|
f = io.BytesIO(b"II*\x00\x08\x00\x00\x00")
|
|
head = f.read(8)
|
|
info = TiffImagePlugin.ImageFileDirectory(head)
|
|
# Should not raise struct.error.
|
|
with pytest.warns(UserWarning):
|
|
info.load(f)
|
|
|
|
|
|
def test_iccprofile(tmp_path: Path) -> None:
|
|
# https://github.com/python-pillow/Pillow/issues/1462
|
|
out = str(tmp_path / "temp.tiff")
|
|
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
|
|
im.save(out)
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert not isinstance(im.info["icc_profile"], tuple)
|
|
assert im.info["icc_profile"] == reloaded.info["icc_profile"]
|
|
|
|
|
|
def test_iccprofile_binary() -> None:
|
|
# https://github.com/python-pillow/Pillow/issues/1526
|
|
# We should be able to load this,
|
|
# but probably won't be able to save it.
|
|
|
|
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
|
assert im.tag_v2.tagtype[34675] == 1
|
|
assert im.info["icc_profile"]
|
|
|
|
|
|
def test_iccprofile_save_png(tmp_path: Path) -> None:
|
|
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
|
|
outfile = str(tmp_path / "temp.png")
|
|
im.save(outfile)
|
|
|
|
|
|
def test_iccprofile_binary_save_png(tmp_path: Path) -> None:
|
|
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
|
outfile = str(tmp_path / "temp.png")
|
|
im.save(outfile)
|
|
|
|
|
|
def test_exif_div_zero(tmp_path: Path) -> None:
|
|
im = hopper()
|
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
|
info[41988] = TiffImagePlugin.IFDRational(0, 0)
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info, compression="raw")
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert 0 == reloaded.tag_v2[41988].numerator
|
|
assert 0 == reloaded.tag_v2[41988].denominator
|
|
|
|
|
|
def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
|
im = hopper()
|
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
|
|
|
max_long = 2**32 - 1
|
|
|
|
# 4 bytes unsigned long
|
|
numerator = max_long
|
|
|
|
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info, compression="raw")
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert max_long == reloaded.tag_v2[41493].numerator
|
|
assert 1 == reloaded.tag_v2[41493].denominator
|
|
|
|
# out of bounds of 4 byte unsigned long
|
|
numerator = max_long + 1
|
|
|
|
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info, compression="raw")
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert max_long == reloaded.tag_v2[41493].numerator
|
|
assert 1 == reloaded.tag_v2[41493].denominator
|
|
|
|
|
|
def test_ifd_signed_rational(tmp_path: Path) -> None:
|
|
im = hopper()
|
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
|
|
|
# pair of 4 byte signed longs
|
|
numerator = 2**31 - 1
|
|
denominator = -(2**31)
|
|
|
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info, compression="raw")
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert numerator == reloaded.tag_v2[37380].numerator
|
|
assert denominator == reloaded.tag_v2[37380].denominator
|
|
|
|
numerator = -(2**31)
|
|
denominator = 2**31 - 1
|
|
|
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info, compression="raw")
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert numerator == reloaded.tag_v2[37380].numerator
|
|
assert denominator == reloaded.tag_v2[37380].denominator
|
|
|
|
# out of bounds of 4 byte signed long
|
|
numerator = -(2**31) - 1
|
|
denominator = 1
|
|
|
|
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info, compression="raw")
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
|
assert -1 == reloaded.tag_v2[37380].denominator
|
|
|
|
|
|
def test_ifd_signed_long(tmp_path: Path) -> None:
|
|
im = hopper()
|
|
info = TiffImagePlugin.ImageFileDirectory_v2()
|
|
|
|
info[37000] = -60000
|
|
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out, tiffinfo=info, compression="raw")
|
|
|
|
with Image.open(out) as reloaded:
|
|
assert reloaded.tag_v2[37000] == -60000
|
|
|
|
|
|
def test_empty_values() -> None:
|
|
data = io.BytesIO(
|
|
b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00"
|
|
b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
b"\x98\x82\x02\x00\x07\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00a "
|
|
b"text\x00\x00"
|
|
)
|
|
head = data.read(8)
|
|
info = TiffImagePlugin.ImageFileDirectory_v2(head)
|
|
info.load(data)
|
|
# Should not raise ValueError.
|
|
info = dict(info)
|
|
assert 33432 in info
|
|
|
|
|
|
def test_photoshop_info(tmp_path: Path) -> None:
|
|
with Image.open("Tests/images/issue_2278.tif") as im:
|
|
assert len(im.tag_v2[34377]) == 70
|
|
assert isinstance(im.tag_v2[34377], bytes)
|
|
out = str(tmp_path / "temp.tiff")
|
|
im.save(out)
|
|
with Image.open(out) as reloaded:
|
|
assert len(reloaded.tag_v2[34377]) == 70
|
|
assert isinstance(reloaded.tag_v2[34377], bytes)
|
|
|
|
|
|
def test_too_many_entries() -> None:
|
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
|
|
|
# 277: ("SamplesPerPixel", SHORT, 1),
|
|
ifd._tagdata[277] = struct.pack("<hh", 4, 4)
|
|
ifd.tagtype[277] = TiffTags.SHORT
|
|
|
|
# Should not raise ValueError.
|
|
with pytest.warns(UserWarning):
|
|
assert ifd[277] == 4
|
|
|
|
|
|
def test_tag_group_data() -> None:
|
|
base_ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
|
interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965)
|
|
for ifd in (base_ifd, interop_ifd):
|
|
ifd[2] = "test"
|
|
ifd[256] = 10
|
|
|
|
assert base_ifd.tagtype[256] == 4
|
|
assert interop_ifd.tagtype[256] != base_ifd.tagtype[256]
|
|
|
|
assert interop_ifd.tagtype[2] == 7
|
|
assert base_ifd.tagtype[2] != interop_ifd.tagtype[256]
|
|
|
|
|
|
def test_empty_subifd(tmp_path: Path) -> None:
|
|
out = str(tmp_path / "temp.jpg")
|
|
|
|
im = hopper()
|
|
exif = im.getexif()
|
|
exif[TiffImagePlugin.EXIFIFD] = {}
|
|
im.save(out, exif=exif)
|
|
|
|
with Image.open(out) as reloaded:
|
|
exif = reloaded.getexif()
|
|
assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {}
|