Skip to content

Commit

Permalink
ctypes: support adding custom tags with variable length
Browse files Browse the repository at this point in the history
In libtiff, custom tags with variable length (eg, a binary blob) are indicated with
the special constants TIFF_VARIABLE (-1) or TIFF_VARIABLE2 (-3). In such
case, when reading a field the length is passed as first argument, and
the data as a pointer as second argument.

So far, some standard tags with such format were supported, but not
custom tags.
=> Add support for such tags.

Also fix the names of the attribute created automatically, which got
broken in the switch from Python 2 to 3.
What should have been named, for instance, TIFFTAG_CUSTOM, was named
like TIFFTAG_b'CUSTOM', quotes included!

Also fix the test cases, which were not working, mostly related to
reading/writing tags.
  • Loading branch information
pieleric committed Jan 11, 2024
1 parent 0537b23 commit 363f5ec
Showing 1 changed file with 63 additions and 38 deletions.
101 changes: 63 additions & 38 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 @@ -340,17 +345,29 @@ def extender_pyfunc(tiff_struct):
def add_tags(tag_list):
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)
data_t = ttype2ctype[field_info.field_type]
convert_c_to_py = lambda d: d.value

# Handle arrays (except for ASCII arrays aka strings, because they are automatically handled)
if (field_info.field_readcount != 1
and field_info.field_type != TIFFDataType.TIFF_ASCII
):
# Note: typically field_readcount == field_writecount
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):
count_t = ctypes.c_uint16 if field_info.field_readcount == TIFF_VARIABLE else 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

tifftags[field_info.field_tag] = (data_t, convert_c_to_py)

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

return TIFFExtender(tag_list_array)

Expand Down Expand Up @@ -418,7 +435,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
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 +1390,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 +1481,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 @@ -1953,16 +1972,16 @@ def suppress_warnings():
def suppress_errors():
libtiff.TIFFSetErrorHandler(_null_error_handler)


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")
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], count=11)
a.SetField("XPOSITION", 42.0)
a.SetField("PRIMARYCHROMATICITIES", (1.0, 2, 3, 4, 5, 6))

Expand All @@ -1983,17 +2002,19 @@ 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"
assert tmp == 42, f"LibtiffTestbyte was not read as 42"
tmp = a.GetField("LibtiffTestuint16")
assert tmp == 42, "LibtiffTestuint16 was not read as 42"
tmp = a.GetField("LibtiffTestMultiuint32")
assert tmp == [1, 2, 3, 4, 5, 6, 7, 8, 9,
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 +2024,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 +2278,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 +2348,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 +2378,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 +2527,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 +2539,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

0 comments on commit 363f5ec

Please sign in to comment.