Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 63 additions & 12 deletions PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,11 @@ def _setitem(self, tag, value, legacy_api):
if info.length == 1:
if legacy_api and self.tagtype[tag] in [5, 10]:
values = values,
dest[tag], = values
try:
dest[tag], = values
except ValueError:
# there's a mismatch between the spec and the item from the file.
dest[tag] = values
else:
dest[tag] = values

Expand Down Expand Up @@ -666,11 +670,22 @@ def load(self, fp):
try:
for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]):
tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12))
taginfo = TiffTags.lookup(tag)
if DEBUG:
tagname = TiffTags.lookup(tag).name
typname = TYPES.get(typ, "unknown")
print("tag: %s (%d) - type: %s (%d)" %
(tagname, tag, typname, typ), end=" ")
print("tag: %s (%d) - type: %s (%d) - spec type: %s - count: %d, spec ct: %d" %
(taginfo.name, tag, typname, typ,
taginfo.type, count, taginfo.length), end=" ")

if (taginfo.type and typ != taginfo.type):
warnings.warn(("Possibly corrupt EXIF data. " +
"File tag type %s does not match spec %s for tag %s"
) % (typ, taginfo.type, tag))

if (taginfo.length and count != taginfo.length):
warnings.warn(("Possibly corrupt EXIF data. " +
"File tag count %s does not match spec %s for tag %s"
) % (count, taginfo.length, tag))

try:
unit_size, handler = self._load_dispatch[typ]
Expand Down Expand Up @@ -1134,6 +1149,8 @@ def _setup(self):
print("- size:", self.size)

sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,))
if not isinstance(sampleFormat, tuple):
sampleFormat = (sampleFormat,)
if (len(sampleFormat) > 1
and max(sampleFormat) == min(sampleFormat) == 1):
# SAMPLEFORMAT is properly per band, so an RGB image will
Expand Down Expand Up @@ -1430,14 +1447,26 @@ def _save(im, fp, filename):
# based on the data in the strip.
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS]
atts = {}
# atts is a dict of key: tuple(type, array, count, value)
# where type is the tifftype int
# array is 0/1 for single or array value
# count is the number of items
# value is the value, or a tuple of the items.
# Note that we've got some items where there's an unspecified length
# in the spec (or 0), and they may have 1 item, so they need to be
# passed in in the array interface as an array of one item.

# bits per sample is a single short in the tiff directory, not a list.
atts[BITSPERSAMPLE] = bits[0]
tag_info = TiffTags.lookup(BITSPERSAMPLE)
atts[BITSPERSAMPLE] = (tag_info.type, 0, 1, bits[0])
# Merge the ones that we have with (optional) more bits from
# the original file, e.g x,y resolution so that we can
# save(load('')) == original file.
legacy_ifd = {}
if hasattr(im, 'tag'):
legacy_ifd = im.tag.to_v2()
if DEBUG:
print("Legacy IFD items: %s" % sorted(legacy_ifd.items()))
for tag, value in itertools.chain(ifd.items(),
getattr(im, 'tag_v2', {}).items(),
legacy_ifd.items()):
Expand All @@ -1448,16 +1477,38 @@ def _save(im, fp, filename):
# UNDONE -- add code for the custom dictionary
if tag not in TiffTags.LIBTIFF_CORE:
continue
if tag not in atts and tag not in blocklist:
if isinstance(value, unicode if bytes is str else str):
atts[tag] = value.encode('ascii', 'replace') + b"\0"
elif isinstance(value, IFDRational):
atts[tag] = float(value)
else:
atts[tag] = value
if tag in atts:
continue
if tag in blocklist:
continue
tag_info = TiffTags.lookup(tag)
# numeric types
if tag_info.length == 1:
if tag_info.type in (3,4,6,8,9):
atts[tag] = (tag_info.type, 0, 1, int(value))
elif tag_info.type in (5,10,11,12):
atts[tag] = (tag_info.type, 0, 1, float(value))
elif tag_info.type == 2:
if isinstance(value, unicode if bytes is str else str):
atts[tag] = (tag_info.type, 0, 1,
value.encode('ascii', 'replace') + b"\0")
else:
atts[tag] = (tag_info.type, 0, 1, value)
# we're not sending bytes to libtiff, as they require custom fields.
#elif tag_info.type == 7:
# atts[tag] = (tag_info.type, 0, 1, value)

else: # undefined or set length.
if tag_info.type in (3,4,6,8,9):
atts[tag] = (tag_info.type, 1, len(value), tuple(map(int,value)))
elif tag_info.type in (5,10,11,12):
atts[tag] = (tag_info.type, 1, len(value), tuple(map(float,value)))
# stringish types


if DEBUG:
print("Converted items: %s" % sorted(atts.items()))
print("Length: %s" % len(atts))

# libtiff always expects the bytes in native order.
# we're storing image byte order. So, if the rawmode
Expand Down
31 changes: 20 additions & 11 deletions PIL/TiffTags.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def lookup(tag):
278: ("RowsPerStrip", LONG, 1),
279: ("StripByteCounts", LONG, 0),

280: ("MinSampleValue", LONG, 0),
281: ("MaxSampleValue", SHORT, 0),
280: ("MinSampleValue", LONG, 1),
281: ("MaxSampleValue", SHORT, 1),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

282: ("XResolution", RATIONAL, 1),
283: ("YResolution", RATIONAL, 1),
284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}),
Expand All @@ -121,7 +121,7 @@ def lookup(tag):
316: ("HostComputer", ASCII, 1),
317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}),
318: ("WhitePoint", RATIONAL, 2),
319: ("PrimaryChromaticities", SHORT, 6),
319: ("PrimaryChromaticities", RATIONAL, 6),

320: ("ColorMap", SHORT, 0),
321: ("HalftoneHints", SHORT, 2),
Expand All @@ -130,16 +130,17 @@ def lookup(tag):
324: ("TileOffsets", LONG, 0),
325: ("TileByteCounts", LONG, 0),

330: ("SubIFD", SHORT, 1),
Copy link
Member

@radarhere radarhere Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

332: ("InkSet", SHORT, 1),
333: ("InkNames", ASCII, 1),
334: ("NumberOfInks", SHORT, 1),
336: ("DotRange", SHORT, 0),
337: ("TargetPrinter", ASCII, 1),
338: ("ExtraSamples", SHORT, 0),
339: ("SampleFormat", SHORT, 0),
339: ("SampleFormat", SHORT, 1),

340: ("SMinSampleValue", DOUBLE, 0),
341: ("SMaxSampleValue", DOUBLE, 0),
340: ("SMinSampleValue", DOUBLE, 1),
341: ("SMaxSampleValue", DOUBLE, 1),
Copy link
Member

@radarhere radarhere Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

342: ("TransferRange", SHORT, 6),

# obsolete JPEG tags
Expand All @@ -156,7 +157,13 @@ def lookup(tag):
529: ("YCbCrCoefficients", RATIONAL, 3),
530: ("YCbCrSubSampling", SHORT, 2),
531: ("YCbCrPositioning", SHORT, 1),
532: ("ReferenceBlackWhite", LONG, 0),
532: ("ReferenceBlackWhite", RATIONAL, 6),

# sgi, in core
32995:("Matteing", SHORT, 1),
32996:("DataType", SHORT, 1),
32997:("ImageDepth", LONG, 1),
32998:("TileDepth", LONG, 1),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at https://gitlab.com/libtiff/libtiff/-/blob/master/libtiff/tif_dirinfo.c?ref_type=heads#L136, I've concluded "DataType" should be 0 rather than 1.

I've created #9245 to add these four tags.


33432: ("Copyright", ASCII, 1),

Expand Down Expand Up @@ -417,23 +424,25 @@ def _populate():
# 393: case TIFFTAG_INKNAMES:

# some of these are not in our TAGS_V2 dict and were included from tiff.h
# Anything included here needs to have the correct type in TAGS_V2 above

LIBTIFF_CORE = {255, 256, 257, 258, 259, 262, 263, 266, 274, 277,
278, 280, 281, 340, 341, 282, 283, 284, 286, 287,
296, 297, 321, 320, 338, 32995, 322, 323, 32998,
32996, 339, 32997, 330, 531, 530, 301, 532, 333,
# as above
269 # this has been in our tests forever, and works
269, # this has been in our tests forever, and works
318, # Whitepoint, Specific test for it
319, # Primary Chromaticities
}

LIBTIFF_CORE.remove(320) # Array of short, crashes
LIBTIFF_CORE.remove(301) # Array of short, crashes
LIBTIFF_CORE.remove(532) # Array of long, crashes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were since removed in #5384

LIBTIFF_CORE.remove(330) # subifd, requires extra support for uint64 payload
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that #5120 blocked the tag when saving with libtiff, I don't think this is needed anymore.


LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes
LIBTIFF_CORE.remove(322) # We don't have support for tiled images in libtiff
LIBTIFF_CORE.remove(323) # Tiled images
LIBTIFF_CORE.remove(333) # Ink Names either
LIBTIFF_CORE.remove(301) # Transfer Function. No support as of yet.

# Note to advanced users: There may be combinations of these
# parameters and values that when added properly, will work and
Expand Down
36 changes: 35 additions & 1 deletion Tests/test_file_libtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def test_additional_metadata(self):
del(core_items[tag])
except:
pass
del(core_items[320]) # colormap is special, tested below

# Type codes:
# 2: "ascii",
Expand Down Expand Up @@ -222,11 +223,21 @@ def test_additional_metadata(self):

out = self.tempfile("temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True

im.save(out, tiffinfo=new_ifd)

TiffImagePlugin.WRITE_LIBTIFF = False

def test_int_dpi(self):
# issue #1765
im = hopper('RGB')
out = self.tempfile('temp.tif')
TiffImagePlugin.WRITE_LIBTIFF = True
im.save(out, dpi=(72, 72))
TiffImagePlugin.WRITE_LIBTIFF = False
reloaded = Image.open(out)
self.assertEqual(reloaded.info['dpi'], (72.0, 72.0))

def test_g3_compression(self):
i = Image.open('Tests/images/hopper_g4_500.tif')
out = self.tempfile("temp.tif")
Expand Down Expand Up @@ -354,6 +365,19 @@ def test_cmyk_save(self):
im2 = Image.open(out)
self.assert_image_equal(im, im2)

def test_palette_save(self):
im = hopper('P')
out = self.tempfile('temp.tif')
TiffImagePlugin.WRITE_LIBTIFF = True
im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False

reloaded = Image.open(out)
# colormap/palette tag
self.assertTrue(len(reloaded.tag_v2[320]), 768)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertTrue(len(reloaded.tag_v2[320]), 768)
self.assertEqual(len(reloaded.tag_v2[320]), 768)

self.assert_image_equal(im, reloaded)


def xtest_bw_compression_w_rgb(self):
""" This test passes, but when running all tests causes a failure due
to output on stderr from the error thrown by libtiff. We need to
Expand Down Expand Up @@ -493,6 +517,16 @@ def test_crashing_metadata(self):
im.save(out, format='TIFF')
TiffImagePlugin.WRITE_LIBTIFF = False

reloaded = Image.open(out)

self.assert_image_equal(im, reloaded)
for tag in (318, 319):
for ix in range(len(im.tag_v2[tag])):
self.assertAlmostEqual(float(im.tag_v2[tag][ix]),
float(reloaded.tag_v2[tag][ix]),
places=5)


def test_page_number_x_0(self):
# Issue 973
# Test TIFF with tag 297 (Page Number) having value of 0 0.
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/internal_design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Internal Reference Docs

open_files
limits
tiff_metadata

Loading