Skip to content

Commit

Permalink
Improve encoding of TIFF tags
Browse files Browse the repository at this point in the history
- 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).
  • Loading branch information
olt authored and radarhere committed Jun 29, 2019
1 parent 08c4792 commit be6fc06
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 189 deletions.
62 changes: 51 additions & 11 deletions Tests/test_file_libtiff.py
Expand Up @@ -3,6 +3,7 @@
from PIL import features
from PIL._util import py3

from collections import namedtuple
from ctypes import c_float
import io
import logging
Expand Down Expand Up @@ -235,12 +236,39 @@ def test_additional_metadata(self):
TiffImagePlugin.WRITE_LIBTIFF = False

def test_custom_metadata(self):
tc = namedtuple("test_case", "value,type,supported_by_default")
custom = {
37000: [4, TiffTags.SHORT],
37001: [4.2, TiffTags.RATIONAL],
37002: ["custom tag value", TiffTags.ASCII],
37003: [u"custom tag value", TiffTags.ASCII],
37004: [b"custom tag value", TiffTags.BYTE],
37000 + k: v
for k, v in enumerate(
[
tc(4, TiffTags.SHORT, True),
tc(123456789, TiffTags.LONG, True),
tc(-4, TiffTags.SIGNED_BYTE, False),
tc(-4, TiffTags.SIGNED_SHORT, False),
tc(-123456789, TiffTags.SIGNED_LONG, False),
tc(TiffImagePlugin.IFDRational(4, 7), TiffTags.RATIONAL, True),
tc(4.25, TiffTags.FLOAT, True),
tc(4.25, TiffTags.DOUBLE, True),
tc("custom tag value", TiffTags.ASCII, True),
tc(u"custom tag value", TiffTags.ASCII, True),
tc(b"custom tag value", TiffTags.BYTE, True),
tc((4, 5, 6), TiffTags.SHORT, True),
tc((123456789, 9, 34, 234, 219387, 92432323), TiffTags.LONG, True),
tc((-4, 9, 10), TiffTags.SIGNED_BYTE, False),
tc((-4, 5, 6), TiffTags.SIGNED_SHORT, False),
tc(
(-123456789, 9, 34, 234, 219387, -92432323),
TiffTags.SIGNED_LONG,
False,
),
tc((4.25, 5.25), TiffTags.FLOAT, True),
tc((4.25, 5.25), TiffTags.DOUBLE, True),
# array of TIFF_BYTE requires bytes instead of tuple for backwards
# compatibility
tc(bytes([4]), TiffTags.BYTE, True),
tc(bytes((4, 9, 10)), TiffTags.BYTE, True),
]
)
}

libtiff_version = TiffImagePlugin._libtiff_version()
Expand All @@ -263,8 +291,13 @@ def check_tags(tiffinfo):
reloaded = Image.open(out)
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
if isinstance(reloaded_value, TiffImagePlugin.IFDRational):
reloaded_value = float(reloaded_value)
if (
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
and libtiff
):
# libtiff does not support real RATIONALS
self.assertAlmostEqual(float(reloaded_value), float(value))
continue

if libtiff and isinstance(value, bytes):
value = value.decode()
Expand All @@ -274,12 +307,19 @@ def check_tags(tiffinfo):
# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata[0]
ifd.tagtype[tag] = tagdata[1]
ifd[tag] = tagdata.value
ifd.tagtype[tag] = tagdata.type
check_tags(ifd)

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

def test_int_dpi(self):
Expand Down
14 changes: 10 additions & 4 deletions docs/handbook/image-file-formats.rst
Expand Up @@ -716,14 +716,20 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may
be passed in this field. However, this is deprecated.

.. versionadded:: 3.0.0

.. note::
.. versionadded:: 5.4.0

Only some tags are currently supported when writing using
Previous versions only supported some tags when writing using
libtiff. The supported list is found in
:py:attr:`~PIL:TiffTags.LIBTIFF_CORE`.

.. versionadded:: 6.1.0

Added support for signed types (e.g. ``TIFF_SIGNED_LONG``) and multiple values.
Multiple values for a single tag must be to
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` as a tuple and
require a matching type in
:py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` tagtype.

**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression
Expand Down
34 changes: 26 additions & 8 deletions src/PIL/TiffImagePlugin.py
Expand Up @@ -99,6 +99,7 @@
Y_RESOLUTION = 283
PLANAR_CONFIGURATION = 284
RESOLUTION_UNIT = 296
TRANSFERFUNCTION = 301
SOFTWARE = 305
DATE_TIME = 306
ARTIST = 315
Expand All @@ -108,6 +109,7 @@
EXTRASAMPLES = 338
SAMPLEFORMAT = 339
JPEGTABLES = 347
REFERENCEBLACKWHITE = 532
COPYRIGHT = 33432
IPTC_NAA_CHUNK = 33723 # newsphoto properties
PHOTOSHOP_CHUNK = 34377 # photoshop properties
Expand Down Expand Up @@ -1538,9 +1540,21 @@ def _save(im, fp, filename):
except io.UnsupportedOperation:
pass

# optional types for non core tags
types = {}
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
# based on the data in the strip.
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS]
# The other tags expect arrays with a certain length (fixed or depending on
# BITSPERSAMPLE, etc), passing arrays with a different length will result in
# segfaults. Block these tags until we add extra validation.
blocklist = [
COLORMAP,
REFERENCEBLACKWHITE,
STRIPBYTECOUNTS,
STRIPOFFSETS,
TRANSFERFUNCTION,
]

atts = {}
# bits per sample is a single short in the tiff directory, not a list.
atts[BITSPERSAMPLE] = bits[0]
Expand All @@ -1555,15 +1569,19 @@ def _save(im, fp, filename):
):
# Libtiff can only process certain core items without adding
# them to the custom dictionary.
# Support for custom items has only been been added
# for int, float, unicode, string and byte values
# Custom items are supported for int, float, unicode, string and byte
# values. Other types and tuples require a tagtype.
if tag not in TiffTags.LIBTIFF_CORE:
if TiffTags.lookup(tag).type == TiffTags.UNDEFINED:
continue
if (
distutils.version.StrictVersion(_libtiff_version())
< distutils.version.StrictVersion("4.0")
) or not (
if distutils.version.StrictVersion(
_libtiff_version()
) < distutils.version.StrictVersion("4.0"):
continue

if tag in ifd.tagtype:
types[tag] = ifd.tagtype[tag]
elif not (
isinstance(value, (int, float, str, bytes))
or (not py3 and isinstance(value, unicode)) # noqa: F821
):
Expand All @@ -1586,7 +1604,7 @@ def _save(im, fp, filename):
if im.mode in ("I;16B", "I;16"):
rawmode = "I;16N"

a = (rawmode, compression, _fp, filename, atts)
a = (rawmode, compression, _fp, filename, atts, types)
e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig)
e.setimage(im.im, (0, 0) + im.size)
while True:
Expand Down

0 comments on commit be6fc06

Please sign in to comment.