Skip to content

Commit be6fc06

Browse files
oltradarhere
authored andcommitted
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 08c4792 commit be6fc06

File tree

6 files changed

+413
-189
lines changed

6 files changed

+413
-189
lines changed

Tests/test_file_libtiff.py

+51-11
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
@@ -235,12 +236,39 @@ def test_additional_metadata(self):
235236
TiffImagePlugin.WRITE_LIBTIFF = False
236237

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

246274
libtiff_version = TiffImagePlugin._libtiff_version()
@@ -263,8 +291,13 @@ def check_tags(tiffinfo):
263291
reloaded = Image.open(out)
264292
for tag, value in tiffinfo.items():
265293
reloaded_value = reloaded.tag_v2[tag]
266-
if isinstance(reloaded_value, TiffImagePlugin.IFDRational):
267-
reloaded_value = float(reloaded_value)
294+
if (
295+
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
296+
and libtiff
297+
):
298+
# libtiff does not support real RATIONALS
299+
self.assertAlmostEqual(float(reloaded_value), float(value))
300+
continue
268301

269302
if libtiff and isinstance(value, bytes):
270303
value = value.decode()
@@ -274,12 +307,19 @@ def check_tags(tiffinfo):
274307
# Test with types
275308
ifd = TiffImagePlugin.ImageFileDirectory_v2()
276309
for tag, tagdata in custom.items():
277-
ifd[tag] = tagdata[0]
278-
ifd.tagtype[tag] = tagdata[1]
310+
ifd[tag] = tagdata.value
311+
ifd.tagtype[tag] = tagdata.type
279312
check_tags(ifd)
280313

281-
# Test without types
282-
check_tags({tag: tagdata[0] for tag, tagdata in custom.items()})
314+
# Test without types. This only works for some types, int for example are
315+
# always encoded as LONG and not SIGNED_LONG.
316+
check_tags(
317+
{
318+
tag: tagdata.value
319+
for tag, tagdata in custom.items()
320+
if tagdata.supported_by_default
321+
}
322+
)
283323
TiffImagePlugin.WRITE_LIBTIFF = False
284324

285325
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

+26-8
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
Y_RESOLUTION = 283
100100
PLANAR_CONFIGURATION = 284
101101
RESOLUTION_UNIT = 296
102+
TRANSFERFUNCTION = 301
102103
SOFTWARE = 305
103104
DATE_TIME = 306
104105
ARTIST = 315
@@ -108,6 +109,7 @@
108109
EXTRASAMPLES = 338
109110
SAMPLEFORMAT = 339
110111
JPEGTABLES = 347
112+
REFERENCEBLACKWHITE = 532
111113
COPYRIGHT = 33432
112114
IPTC_NAA_CHUNK = 33723 # newsphoto properties
113115
PHOTOSHOP_CHUNK = 34377 # photoshop properties
@@ -1538,9 +1540,21 @@ def _save(im, fp, filename):
15381540
except io.UnsupportedOperation:
15391541
pass
15401542

1543+
# optional types for non core tags
1544+
types = {}
15411545
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
15421546
# based on the data in the strip.
1543-
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS]
1547+
# The other tags expect arrays with a certain length (fixed or depending on
1548+
# BITSPERSAMPLE, etc), passing arrays with a different length will result in
1549+
# segfaults. Block these tags until we add extra validation.
1550+
blocklist = [
1551+
COLORMAP,
1552+
REFERENCEBLACKWHITE,
1553+
STRIPBYTECOUNTS,
1554+
STRIPOFFSETS,
1555+
TRANSFERFUNCTION,
1556+
]
1557+
15441558
atts = {}
15451559
# bits per sample is a single short in the tiff directory, not a list.
15461560
atts[BITSPERSAMPLE] = bits[0]
@@ -1555,15 +1569,19 @@ def _save(im, fp, filename):
15551569
):
15561570
# Libtiff can only process certain core items without adding
15571571
# them to the custom dictionary.
1558-
# Support for custom items has only been been added
1559-
# for int, float, unicode, string and byte values
1572+
# Custom items are supported for int, float, unicode, string and byte
1573+
# values. Other types and tuples require a tagtype.
15601574
if tag not in TiffTags.LIBTIFF_CORE:
15611575
if TiffTags.lookup(tag).type == TiffTags.UNDEFINED:
15621576
continue
1563-
if (
1564-
distutils.version.StrictVersion(_libtiff_version())
1565-
< distutils.version.StrictVersion("4.0")
1566-
) or not (
1577+
if distutils.version.StrictVersion(
1578+
_libtiff_version()
1579+
) < distutils.version.StrictVersion("4.0"):
1580+
continue
1581+
1582+
if tag in ifd.tagtype:
1583+
types[tag] = ifd.tagtype[tag]
1584+
elif not (
15671585
isinstance(value, (int, float, str, bytes))
15681586
or (not py3 and isinstance(value, unicode)) # noqa: F821
15691587
):
@@ -1586,7 +1604,7 @@ def _save(im, fp, filename):
15861604
if im.mode in ("I;16B", "I;16"):
15871605
rawmode = "I;16N"
15881606

1589-
a = (rawmode, compression, _fp, filename, atts)
1607+
a = (rawmode, compression, _fp, filename, atts, types)
15901608
e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig)
15911609
e.setimage(im.im, (0, 0) + im.size)
15921610
while True:

0 commit comments

Comments
 (0)