Skip to content

Commit 4c0fbba

Browse files
committed
Improve encoding of TIFF tags
- Pass tagtype from v2 directory to libtiff encoder, instead of autodetecting type. - Use explicit types. E.g. uint32_t for TIFF_LONG to fix issues on platforms with 64bit longs. - Add support for multiple values (arrays). Requires type in v2 directory and values must be passed as a tuple. - Add support for signed types (e.g. TIFFTypes.TIFF_SIGNED_SHORT).
1 parent 14766f5 commit 4c0fbba

File tree

6 files changed

+390
-189
lines changed

6 files changed

+390
-189
lines changed

Tests/test_file_libtiff.py

+40-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from PIL import features
44
from PIL._util import py3
55

6+
from collections import namedtuple
67
from ctypes import c_float
78
import io
89
import logging
@@ -233,13 +234,32 @@ def test_additional_metadata(self):
233234
TiffImagePlugin.WRITE_LIBTIFF = False
234235

235236
def test_custom_metadata(self):
236-
custom = {
237-
37000: [4, TiffTags.SHORT],
238-
37001: [4.2, TiffTags.RATIONAL],
239-
37002: ['custom tag value', TiffTags.ASCII],
240-
37003: [u'custom tag value', TiffTags.ASCII],
241-
37004: [b'custom tag value', TiffTags.BYTE]
242-
}
237+
tc = namedtuple('test_case', 'value,type,supported_by_default')
238+
custom = {37000+k: v for k, v in enumerate([
239+
tc(4, TiffTags.SHORT, True),
240+
tc(123456789, TiffTags.LONG, True),
241+
tc(-4, TiffTags.SIGNED_BYTE, False),
242+
tc(-4, TiffTags.SIGNED_SHORT, False),
243+
tc(-123456789, TiffTags.SIGNED_LONG, False),
244+
tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
245+
tc(4.25, TiffTags.FLOAT, True),
246+
tc(4.25, TiffTags.DOUBLE, True),
247+
tc('custom tag value', TiffTags.ASCII, True),
248+
tc(u'custom tag value', TiffTags.ASCII, True),
249+
tc(b'custom tag value', TiffTags.BYTE, True),
250+
tc((4, 5, 6), TiffTags.SHORT, True),
251+
tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
252+
tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
253+
tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
254+
tc((-123456789, 9, 34, 234, 219387, -92432323),
255+
TiffTags.SIGNED_LONG, False),
256+
tc((4.25, 5.25), TiffTags.FLOAT, True),
257+
tc((4.25, 5.25), TiffTags.DOUBLE, True),
258+
# array of TIFF_BYTE requires bytes instead of tuple for backwards
259+
# compatibility
260+
tc(bytes([4]), TiffTags.BYTE, True),
261+
tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
262+
])}
243263

244264
libtiff_version = TiffImagePlugin._libtiff_version()
245265

@@ -260,8 +280,13 @@ def check_tags(tiffinfo):
260280
reloaded = Image.open(out)
261281
for tag, value in tiffinfo.items():
262282
reloaded_value = reloaded.tag_v2[tag]
263-
if isinstance(reloaded_value, TiffImagePlugin.IFDRational):
264-
reloaded_value = float(reloaded_value)
283+
if (
284+
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
285+
and libtiff
286+
):
287+
# libtiff does not support real RATIONALS
288+
self.assertAlmostEqual(float(reloaded_value), float(value))
289+
continue
265290

266291
if libtiff and isinstance(value, bytes):
267292
value = value.decode()
@@ -271,12 +296,14 @@ def check_tags(tiffinfo):
271296
# Test with types
272297
ifd = TiffImagePlugin.ImageFileDirectory_v2()
273298
for tag, tagdata in custom.items():
274-
ifd[tag] = tagdata[0]
275-
ifd.tagtype[tag] = tagdata[1]
299+
ifd[tag] = tagdata.value
300+
ifd.tagtype[tag] = tagdata.type
276301
check_tags(ifd)
277302

278-
# Test without types
279-
check_tags({tag: tagdata[0] for tag, tagdata in custom.items()})
303+
# Test without types. This only works for some types, int for example are
304+
# always encoded as LONG and not SIGNED_LONG.
305+
check_tags({tag: tagdata.value for tag, tagdata in custom.items()
306+
if tagdata.supported_by_default})
280307
TiffImagePlugin.WRITE_LIBTIFF = False
281308

282309
def test_int_dpi(self):

docs/handbook/image-file-formats.rst

+10-4
Original file line numberDiff line numberDiff line change
@@ -716,14 +716,20 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
716716
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may
717717
be passed in this field. However, this is deprecated.
718718

719-
.. versionadded:: 3.0.0
720-
721-
.. note::
719+
.. versionadded:: 5.4.0
722720

723-
Only some tags are currently supported when writing using
721+
Previous versions only supported some tags when writing using
724722
libtiff. The supported list is found in
725723
:py:attr:`~PIL:TiffTags.LIBTIFF_CORE`.
726724

725+
.. versionadded:: 6.1.0
726+
727+
Added support for signed types (e.g. ``TIFF_SIGNED_LONG``) and multiple values.
728+
Multiple values for a single tag must be to
729+
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` as a tuple and
730+
require a matching type in
731+
:py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` tagtype.
732+
727733
**compression**
728734
A string containing the desired compression method for the
729735
file. (valid only with libtiff installed) Valid compression

src/PIL/TiffImagePlugin.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,8 @@ def _save(im, fp, filename):
15061506
except io.UnsupportedOperation:
15071507
pass
15081508

1509+
# optional types for non core tags
1510+
types = {}
15091511
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
15101512
# based on the data in the strip.
15111513
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS]
@@ -1523,14 +1525,20 @@ def _save(im, fp, filename):
15231525
legacy_ifd.items()):
15241526
# Libtiff can only process certain core items without adding
15251527
# them to the custom dictionary.
1526-
# Support for custom items has only been been added
1527-
# for int, float, unicode, string and byte values
1528+
# Custom items are supported for int, float, unicode, string and byte
1529+
# values. Other types and tuples require a tagtype.
15281530
if tag not in TiffTags.LIBTIFF_CORE:
15291531
if TiffTags.lookup(tag).type == TiffTags.UNDEFINED:
15301532
continue
1531-
if (distutils.version.StrictVersion(_libtiff_version()) <
1532-
distutils.version.StrictVersion("4.0")) \
1533-
or not (isinstance(value, (int, float, str, bytes)) or
1533+
if (
1534+
distutils.version.StrictVersion(_libtiff_version()) <
1535+
distutils.version.StrictVersion("4.0")
1536+
):
1537+
continue
1538+
1539+
if tag in ifd.tagtype:
1540+
types[tag] = ifd.tagtype[tag]
1541+
elif not (isinstance(value, (int, float, str, bytes)) or
15341542
(not py3 and isinstance(value, unicode))): # noqa: F821
15351543
continue
15361544
if tag not in atts and tag not in blocklist:
@@ -1551,7 +1559,7 @@ def _save(im, fp, filename):
15511559
if im.mode in ('I;16B', 'I;16'):
15521560
rawmode = 'I;16N'
15531561

1554-
a = (rawmode, compression, _fp, filename, atts)
1562+
a = (rawmode, compression, _fp, filename, atts, types)
15551563
e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig)
15561564
e.setimage(im.im, (0, 0)+im.size)
15571565
while True:

0 commit comments

Comments
 (0)