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

Improve encoding of TIFF tags #3861

Merged
merged 4 commits into from Jun 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
37 changes: 29 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,24 @@ def _save(im, fp, filename):
except io.UnsupportedOperation:
pass

# optional types for non core tags
types = {}
# SAMPLEFORMAT is determined by the image format and should not be copied
# from legacy_ifd.
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
# based on the data in the strip.
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, SAMPLEFORMAT]
# 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,
SAMPLEFORMAT,
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 +1572,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 +1607,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