Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for variable length custom tags #173

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 103 additions & 37 deletions libtiff/libtiff_ctypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ class c_thandle_t(ctypes.c_void_p):
# types defined for creating custom tags
FIELD_CUSTOM = 65

# Special values for field_readcount & field_writecount
TIFF_VARIABLE = -1 # The length is variable, this number is passed as an uint16
TIFFTAG_SPP = -2 # There are as many values as defined in TIFFTAG_SAMPLESPERPIXEL
TIFF_VARIABLE2 = -3 # The length is variable, this number is passed as an uint32


class TIFFDataType(object):
"""Place holder for the enum in C.
Expand Down Expand Up @@ -289,8 +294,8 @@ class TIFFFieldInfo(ctypes.Structure):
"""
typedef struct {
ttag_t field_tag; /* field's tag */
short field_readcount; /* read count/TIFF_VARIABLE/TIFF_SPP */
short field_writecount; /* write count/TIFF_VARIABLE */
short field_readcount; /* read count/TIFF_VARIABLE/TIFF_VARIABLE2/TIFF_SPP */
short field_writecount; /* write count/TIFF_VARIABLE/TIFF_VARIABLE2*/
TIFFDataType field_type; /* type of associated data */
unsigned short field_bit; /* bit in fieldsset bit vector */
unsigned char field_oktochange; /* if true, can change while writing */
Expand Down Expand Up @@ -338,23 +343,74 @@ def extender_pyfunc(tiff_struct):


def add_tags(tag_list):
pieleric marked this conversation as resolved.
Show resolved Hide resolved
"""
Adds support for reading and writing custom tags.

Parameters
----------
tag_list: List of TIFFFieldInfo.
The definitions of each new tags to support, as defined by libtiff.

Returns
-------
TIFFExtender: the new function that will be used by libtiff to support
the new custom tags.
"""
tag_list_array = (TIFFFieldInfo * len(tag_list))(*tag_list)
for field_info in tag_list_array:
_name = "TIFFTAG_" + str(field_info.field_name).upper()
globals()[_name] = field_info.field_tag
if field_info.field_writecount > 1 and field_info.field_type != \
TIFFDataType.TIFF_ASCII:
tifftags[field_info.field_tag] = (
ttype2ctype[
field_info.field_type] * field_info.field_writecount,
lambda _d: _d.contents[:])
else:
tifftags[field_info.field_tag] = (
ttype2ctype[field_info.field_type], lambda _d: _d.value)
tifftags[field_info.field_tag] = _field_info_to_tifftag(field_info)

name = "TIFFTAG_" + field_info.field_name.decode("ascii").upper()
globals()[name] = field_info.field_tag

return TIFFExtender(tag_list_array)


def _field_info_to_tifftag(field_info):
"""
Creates an entry for tifftags based on a field_info.

Parameters
----------
field_info: TIFFFieldInfo
The definition of the new tag.

Returns
-------
Tuple with: C type of the data (or tuple of C types for the count and data,
if it's a variable length field), and a function to convert from the C
type to a python type.
"""
data_t = ttype2ctype[field_info.field_type]
convert_c_to_py = lambda d: d.value

# Note: typically field_readcount == field_writecount
if field_info.field_readcount != field_info.field_writecount:
warnings.warn(f"Unsupported readcount != writecount "
f"({field_info.field_readcount} != {field_info.field_writecount})")
# Let's be optimistic and assume it'll work as-is

# Handle arrays (except for ASCII arrays aka C strings, because they are automatically handled)
if (field_info.field_readcount != 1
and field_info.field_type != TIFFDataType.TIFF_ASCII
):
if field_info.field_readcount > 1:
data_t = data_t * field_info.field_readcount
convert_c_to_py = lambda d: d.contents[:]
elif field_info.field_readcount in (TIFF_VARIABLE, TIFF_VARIABLE2):
if field_info.field_readcount == TIFF_VARIABLE:
count_t = ctypes.c_uint16
else:
count_t = ctypes.c_uint32
data_t = (count_t, data_t)
convert_c_to_py = lambda d: d[1][:d[0]]
else:
warnings.warn(f"Unsupported readcount {field_info.field_readcount}")
# Let's be optimistic and assume the standard behaviour will work

return (data_t, convert_c_to_py)


tifftags = {

# TODO:
Expand Down Expand Up @@ -418,7 +474,7 @@ def add_tags(tag_list):
TIFFTAG_BITSPERSAMPLE: (ctypes.c_uint16, lambda _d: _d.value),
TIFFTAG_CLEANFAXDATA: (ctypes.c_uint16, lambda _d: _d.value),
TIFFTAG_COMPRESSION: (ctypes.c_uint16, lambda _d: _d.value),
TIFFTAG_DATATYPE: (ctypes.c_uint16, lambda _d: _d.value),
TIFFTAG_DATATYPE: (ctypes.c_uint16, lambda _d: _d.value), # Obsolete tag replaced by SampleFormat
pearu marked this conversation as resolved.
Show resolved Hide resolved
TIFFTAG_FILLORDER: (ctypes.c_uint16, lambda _d: _d.value),
TIFFTAG_INKSET: (ctypes.c_uint16, lambda _d: _d.value),
TIFFTAG_MATTEING: (ctypes.c_uint16, lambda _d: _d.value),
Expand Down Expand Up @@ -1373,17 +1429,19 @@ def GetField(self, tag, ignore_undefined_tag=True, count=None):
tag can be numeric constant TIFFTAG_<tagname> or a
string containing <tagname>.
"""
# Special trick to read extra metadata as text in the ImageDescription
if tag in ['PixelSizeX', 'PixelSizeY', 'RelativeTime']:
descr = self.GetField('ImageDescription')
if not descr:
return
_i = descr.find(tag)
_i = descr.find(tag.encode("ascii"))
if _i == -1:
return
_value = eval(descr[_i + len(tag):].lstrip().split()[0])
return _value

if isinstance(tag, str):
tag = eval('TIFFTAG_' + tag.upper())
tag = globals()['TIFFTAG_' + tag.upper()]
t = tifftags.get(tag)
if t is None:
if not ignore_undefined_tag:
Expand Down Expand Up @@ -1462,7 +1520,7 @@ def SetField(self, tag, _value, count=None):
print("Warning: count argument is deprecated")

if isinstance(tag, str):
tag = eval('TIFFTAG_' + tag.upper())
tag = globals()['TIFFTAG_' + tag.upper()]
t = tifftags.get(tag)
if t is None:
print('Warning: no tag %r defined' % tag)
Expand Down Expand Up @@ -1601,7 +1659,8 @@ def copy(self, filename, **kws):
orig_value = self.GetField(define)
if orig_value is None and define not in define_rewrite:
continue
if _name.endswith('OFFSETS') or _name.endswith('BYTECOUNTS'):
if (_name.endswith('OFFSETS') or _name.endswith('BYTECOUNTS')
or define == TIFFTAG_DATATYPE): # old version of SampleFormat
continue
if define in define_rewrite:
_value = define_rewrite[define]
Expand Down Expand Up @@ -1958,11 +2017,12 @@ def _test_custom_tags():
def _tag_write():
a = TIFF.open("/tmp/libtiff_test_custom_tags.tif", "w")

a.SetField("ARTIST", "MY NAME")
a.SetField("ARTIST", b"MY NAME")
pieleric marked this conversation as resolved.
Show resolved Hide resolved
a.SetField("LibtiffTestByte", 42)
a.SetField("LibtiffTeststr", "FAKE")
a.SetField("LibtiffTeststr", b"FAKE")
a.SetField("LibtiffTestuint16", 42)
a.SetField("LibtiffTestMultiuint32", (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
a.SetField("LibtiffTestBytes", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
a.SetField("XPOSITION", 42.0)
a.SetField("PRIMARYCHROMATICITIES", (1.0, 2, 3, 4, 5, 6))

Expand All @@ -1983,7 +2043,7 @@ def _tag_read():
tmp = a.GetField("XPOSITION")
assert tmp == 42.0, "XPosition was not read as 42.0"
tmp = a.GetField("ARTIST")
assert tmp == "MY NAME", "Artist was not read as 'MY NAME'"
assert tmp == b"MY NAME", "Artist was not read as 'MY NAME'"
tmp = a.GetField("LibtiffTestByte")
assert tmp == 42, "LibtiffTestbyte was not read as 42"
tmp = a.GetField("LibtiffTestuint16")
Expand All @@ -1993,7 +2053,9 @@ def _tag_read():
10], "LibtiffTestMultiuint32 was not read as [1,2,3," \
"4,5,6,7,8,9,10]"
tmp = a.GetField("LibtiffTeststr")
assert tmp == "FAKE", "LibtiffTeststr was not read as 'FAKE'"
assert tmp == b"FAKE", "LibtiffTeststr was not read as 'FAKE'"
tmp = a.GetField("LibtiffTestBytes")
assert tmp == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
tmp = a.GetField("PRIMARYCHROMATICITIES")
assert tmp == [1.0, 2.0, 3.0, 4.0, 5.0,
6.0], "PrimaryChromaticities was not read as [1.0," \
Expand All @@ -2003,13 +2065,15 @@ def _tag_read():
# Define a C structure that says how each tag should be used
test_tags = [
TIFFFieldInfo(40100, 1, 1, TIFFDataType.TIFF_BYTE, FIELD_CUSTOM, True,
False, "LibtiffTestByte"),
False, b"LibtiffTestByte"),
TIFFFieldInfo(40103, 10, 10, TIFFDataType.TIFF_LONG, FIELD_CUSTOM,
True, False, "LibtiffTestMultiuint32"),
True, False, b"LibtiffTestMultiuint32"),
TIFFFieldInfo(40102, 1, 1, TIFFDataType.TIFF_SHORT, FIELD_CUSTOM, True,
False, "LibtiffTestuint16"),
False, b"LibtiffTestuint16"),
TIFFFieldInfo(40101, -1, -1, TIFFDataType.TIFF_ASCII, FIELD_CUSTOM,
True, False, "LibtiffTeststr")
True, False, b"LibtiffTeststr"),
TIFFFieldInfo(40104, TIFF_VARIABLE2, TIFF_VARIABLE2, TIFFDataType.TIFF_BYTE, FIELD_CUSTOM,
True, True, b"LibtiffTestBytes"),
]

# Add tags to the libtiff library
Expand Down Expand Up @@ -2255,15 +2319,15 @@ def _test_read_one_tile():
raise AssertionError(
"An exception must be raised with invalid (x, y) values")
except ValueError as inst:
assert inst.message == "Invalid x value", repr(inst.message)
assert str(inst) == "Invalid x value", inst

# test y greater than the image height
try:
tiff.read_one_tile(0, 5000)
raise AssertionError(
"An exception must be raised with invalid (x, y) values")
except ValueError as inst:
assert inst.message == "Invalid y value", repr(inst.message)
assert str(inst) == "Invalid y value", inst

# RGB image sized 3000 x 2500, PLANARCONFIG_SEPARATE
tiff.SetDirectory(3)
Expand Down Expand Up @@ -2325,9 +2389,9 @@ def assert_image_tag(tiff, tag_name, expected_value):

def _test_tags_write():
tiff = TIFF.open('/tmp/libtiff_tags_write.tiff', mode='w')
tmp = tiff.SetField("Artist", "A Name")
tmp = tiff.SetField("Artist", b"A Name")
assert tmp == 1, "Tag 'Artist' was not written properly"
tmp = tiff.SetField("DocumentName", "")
tmp = tiff.SetField("DocumentName", b"")
assert tmp == 1, "Tag 'DocumentName' with empty string was not written " \
"properly"
tmp = tiff.SetField("PrimaryChromaticities", [1, 2, 3, 4, 5, 6])
Expand Down Expand Up @@ -2355,10 +2419,10 @@ def _test_tags_read(filename=None):
filename = sys.argv[1]
tiff = TIFF.open(filename)
tmp = tiff.GetField("Artist")
assert tmp == "A Name", "Tag 'Artist' did not read the correct value (" \
assert tmp == b"A Name", "Tag 'Artist' did not read the correct value (" \
"Got '%s'; Expected 'A Name')" % (tmp,)
tmp = tiff.GetField("DocumentName")
assert tmp == "", "Tag 'DocumentName' did not read the correct value (" \
assert tmp == b"", "Tag 'DocumentName' did not read the correct value (" \
"Got '%s'; Expected empty string)" % (tmp,)
tmp = tiff.GetField("PrimaryChromaticities")
assert tmp == [1, 2, 3, 4, 5,
Expand Down Expand Up @@ -2504,7 +2568,7 @@ def _test_copy():
arr[_i, j] = 1 + _i + 10 * j
# from scipy.stats import poisson
# arr = poisson.rvs (arr)
tiff.SetField('ImageDescription', 'Hey\nyou')
tiff.SetField('ImageDescription', b'Hey\nyou')
tiff.write_image(arr, compression='lzw')
del tiff

Expand All @@ -2516,16 +2580,18 @@ def _test_copy():

for compression in ['none', 'lzw', 'deflate']:
for sampleformat in ['int', 'uint', 'float']:
for bitspersample in [256, 128, 64, 32, 16, 8]:
if sampleformat == 'float' and (
bitspersample < 32 or bitspersample > 128):
for bitspersample in [128, 64, 32, 16, 8]:
if sampleformat == 'float' and bitspersample < 32:
continue
if sampleformat in ['int', 'uint'] and bitspersample > 64:
continue
# With compression, less data types supported
if compression != 'none' and bitspersample > 32:
continue
# print compression, sampleformat, bitspersample
tiff.copy('/tmp/libtiff_test_copy2.tiff',
compression=compression,
imagedescription='hoo',
imagedescription=b'hoo',
sampleformat=sampleformat,
bitspersample=bitspersample)
tiff2 = TIFF.open('/tmp/libtiff_test_copy2.tiff', mode='r')
Expand Down