Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 174 additions & 19 deletions PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import array, sys
import collections
import itertools
import os

II = b"II" # little-endian (intel-style)
MM = b"MM" # big-endian (motorola-style)
Expand Down Expand Up @@ -121,6 +122,8 @@
32773: "packbits"
}

COMPRESSION_INFO_REV = dict([(v,k) for (k,v) in COMPRESSION_INFO.items()])

OPEN_INFO = {
# (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
# ExtraSamples) => mode, rawmode
Expand Down Expand Up @@ -532,7 +535,12 @@ def _open(self):
self.__frame = -1
self.__fp = self.fp

# and load the first frame
if Image.DEBUG:
print ("*** TiffImageFile._open ***")
print ("- __first:", self.__first)
print ("- ifh: ", ifh)

# and load the first frame
self._seek(0)

def seek(self, frame):
Expand Down Expand Up @@ -567,7 +575,7 @@ def _tell(self):

return self.__frame

def _decoder(self, rawmode, layer):
def _decoder(self, rawmode, layer, tile=None):
"Setup decoder contexts"

args = None
Expand All @@ -594,6 +602,63 @@ def _decoder(self, rawmode, layer):

return args

def _load_libtiff(self):
""" Overload method triggered when we detect a g3/g4 tiff
Calls out to lib tiff """

pixel = Image.Image.load(self)

if self.tile is None:
raise IOError("cannot load this image")
if not self.tile:
return pixel

self.load_prepare()

if not len(self.tile) == 1:
raise IOError("Not exactly one tile")

d, e, o, a = self.tile[0]
d = Image._getdecoder(self.mode, d, a, self.decoderconfig)
try:
d.setimage(self.im, e)
except ValueError:
raise IOError("Couldn't set the image")

if hasattr(self.fp, "fileno"):
# we've got a actual file on disk, pass in the fp.
if Image.DEBUG:
print ("have fileno, calling fileno version of the decoder.")
self.fp.seek(0)
n,e = d.decode("fpfp") # 4 bytes, otherwise the trace might error out
elif hasattr(self.fp, "getvalue"):
# We've got a stringio like thing passed in. Yay for all in memory.
# The decoder needs the entire file in one shot, so there's not
# a lot we can do here other than give it the entire file.
# unless we could do something like get the address of the underlying
# string for stringio.
if Image.DEBUG:
print ("have getvalue. just sending in a string from getvalue")
n,e = d.decode(self.fp.getvalue())
else:
# we have something else.
if Image.DEBUG:
print ("don't have fileno or getvalue. just reading")
# UNDONE -- so much for that buffer size thing.
n, e = d.decode(self.fp.read())


self.tile = []
self.readonly = 0
self.fp = None # might be shared

if e < 0:
raise IOError(e)

self.load_end()

return Image.Image.load(self)

def _setup(self):
"Setup this image object based on current tags"

Expand Down Expand Up @@ -669,20 +734,54 @@ def _setup(self):
self.tile = []
if STRIPOFFSETS in self.tag:
# striped image
offsets = self.tag[STRIPOFFSETS]
h = getscalar(ROWSPERSTRIP, ysize)
w = self.size[0]
a = None
for o in self.tag[STRIPOFFSETS]:
if not a:
a = self._decoder(rawmode, l)
if self._compression in ["tiff_ccitt", "group3",
"group4", "tiff_raw_16"]:
## if Image.DEBUG:
## print "Activating g4 compression for whole file"

# Decoder expects entire file as one tile.
# There's a buffer size limit in load (64k)
# so large g4 images will fail if we use that
# function.
#
# Setup the one tile for the whole image, then
# replace the existing load function with our
# _load_libtiff function.

self.load = self._load_libtiff

# To be nice on memory footprint, if there's a
# file descriptor, use that instead of reading
# into a string in python.

# libtiff closes the file descriptor, so pass in a dup.
fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno())

# Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds
a = (rawmode, self._compression, fp )
self.tile.append(
(self._compression,
(0, min(y, ysize), w, min(y+h, ysize)),
o, a))
y = y + h
if y >= self.size[1]:
x = y = 0
l = l + 1
(0, 0, w, ysize),
0, a))
a = None

else:
for i in range(len(offsets)):
a = self._decoder(rawmode, l, i)
self.tile.append(
(self._compression,
(0, min(y, ysize), w, min(y+h, ysize)),
offsets[i], a))
if Image.DEBUG:
print ("tiles: ", self.tile)
y = y + h
if y >= self.size[1]:
x = y = 0
l = l + 1
a = None
elif TILEOFFSETS in self.tag:
# tiled image
Expand Down Expand Up @@ -764,8 +863,12 @@ def _save(im, fp, filename):

ifd = ImageFileDirectory(prefix)

compression = im.info.get('compression','raw')
libtiff = compression in ["tiff_ccitt", "group3",
"group4", "tiff_raw_16"]

# -- multi-page -- skip TIFF header on subsequent pages
if fp.tell() == 0:
if not libtiff and fp.tell() == 0:
# tiff header (write via IFD to get everything right)
# PIL always starts the first IFD at offset 8
fp.write(ifd.prefix + ifd.o16(42) + ifd.o32(8))
Expand Down Expand Up @@ -842,13 +945,65 @@ def _save(im, fp, filename):
ifd[ROWSPERSTRIP] = im.size[1]
ifd[STRIPBYTECOUNTS] = stride * im.size[1]
ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
ifd[COMPRESSION] = 1 # no compression
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression,1) # no compression by default

offset = ifd.save(fp)

ImageFile._save(im, fp, [
("raw", (0,0)+im.size, offset, (rawmode, stride, 1))
])
if libtiff:
if Image.DEBUG:
print ("Saving using libtiff encoder")
print (ifd.items())
_fp = 0
if hasattr(fp, "fileno"):
fp.seek(0)
_fp = os.dup(fp.fileno())

blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes.
atts = dict([(k,v) for (k,(v,)) in ifd.items() if k not in blocklist])
try:
# pull in more bits from the original file, e.g x,y resolution
# so that we can save(load('')) == original file.
for k,v in im.ifd.items():
if k not in atts and k not in blocklist:
if type(v[0]) == tuple and len(v) > 1:
# A tuple of more than one rational tuples
# flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
atts[k] = [float(elt[0])/float(elt[1]) for elt in v]
continue
if type(v[0]) == tuple and len(v) == 1:
# A tuple of one rational tuples
# flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL
atts[k] = float(v[0][0])/float(v[0][1])
continue
if type(v) == tuple and len(v) == 1:
# int or similar
atts[k] = v[0]
continue
if type(v) == str:
atts[k] = v
continue

except:
# if we don't have an ifd here, just punt.
pass
if Image.DEBUG:
print (atts)
a = (rawmode, compression, _fp, filename, atts)
e = Image._getencoder(im.mode, compression, a, im.encoderconfig)
e.setimage(im.im, (0,0)+im.size)
while 1:
l, s, d = e.encode(16*1024) # undone, change to self.decodermaxblock
if not _fp:
fp.write(d)
if s:
break
if s < 0:
raise IOError("encoder error %d when writing image file" % s)

else:
offset = ifd.save(fp)

ImageFile._save(im, fp, [
("raw", (0,0)+im.size, offset, (rawmode, stride, 1))
])


# -- helper for multi-page save --
Expand Down
Binary file added Tests/images/lena_bw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/lena_bw_500.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/lena_g4.tif
Binary file not shown.
Binary file added Tests/images/lena_g4_500.tif
Binary file not shown.
Binary file added Tests/images/pport_g4.tif
Binary file not shown.
98 changes: 98 additions & 0 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from PIL import Image

import StringIO
import random

def test_sanity():

file = tempfile("temp.tif")
Expand Down Expand Up @@ -55,3 +58,98 @@ def test_gimp_tiff():
('jpeg', (0, 192, 256, 256), 3890, ('RGB', '')),
])
assert_no_exception(lambda: im.load())

def _assert_noerr(im):
"""Helper tests that assert basic sanity about the g4 tiff reading"""
#1 bit
assert_equal(im.mode, "1")

# Does the data actually load
assert_no_exception(lambda: im.load())
assert_no_exception(lambda: im.getdata())

try:
assert_equal(im._compression, 'group4')
except:
print "No _compression"
print (dir(im))

# can we write it back out, in a different form.
out = tempfile("temp.png")
assert_no_exception(lambda: im.save(out))

def test_g4_tiff():
"""Test the ordinary file path load path"""

file = "Tests/images/lena_g4_500.tif"
im = Image.open(file)

assert_equal(im.size, (500,500))
_assert_noerr(im)

def test_g4_large():
file = "Tests/images/pport_g4.tif"
im = Image.open(file)
_assert_noerr(im)

def test_g4_tiff_file():
"""Testing the string load path"""

file = "Tests/images/lena_g4_500.tif"
with open(file,'rb') as f:
im = Image.open(f)

assert_equal(im.size, (500,500))
_assert_noerr(im)

def test_g4_tiff_stringio():
"""Testing the stringio loading code path"""

file = "Tests/images/lena_g4_500.tif"
s = StringIO.StringIO()
with open(file,'rb') as f:
s.write(f.read())
s.seek(0)
im = Image.open(s)

assert_equal(im.size, (500,500))
_assert_noerr(im)

def test_g4_tiff_fail(): # UNDONE fails badly, unknown reason
"""The 128x128 lena image fails for some reason. Investigating"""

Image.DEBUG = True
file = "Tests/images/lena_g4.tif"
im = Image.open(file)

assert_equal(im.size, (128,128))
_assert_noerr(im)
Image.DEBUG = False

def test_g4_eq_png():
""" Checking that we're actually getting the data that we expect"""
png = Image.open('Tests/images/lena_bw_500.png')
g4 = Image.open('Tests/images/lena_g4_500.tif')

assert_image_equal(g4, png)

def test_g4_write():
"""Checking to see that the saved image is the same as what we wrote"""
Image.DEBUG = True

file = "Tests/images/lena_g4_500.tif"
orig = Image.open(file)

out = "temp.tif"
rot = orig.transpose(Image.ROTATE_90)
assert_equal(rot.size,(500,500))
rot.save(out)

reread = Image.open(out)
assert_equal(reread.size,(500,500))
_assert_noerr(reread)
assert_image_equal(reread, rot)

assert_false(orig.tobytes() == reread.tobytes())

Image.DEBUG = False
13 changes: 13 additions & 0 deletions _imaging.c
Original file line number Diff line number Diff line change
Expand Up @@ -3236,6 +3236,7 @@ extern PyObject* PyImaging_GifDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_HexDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_JpegDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_TiffLzwDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_MspDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_PackbitsDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_PcdDecoderNew(PyObject* self, PyObject* args);
Expand All @@ -3254,6 +3255,7 @@ extern PyObject* PyImaging_PcxEncoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_RawEncoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_XbmEncoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_ZipEncoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args);

/* Display support etc (in display.c) */
#ifdef WIN32
Expand Down Expand Up @@ -3303,6 +3305,17 @@ static PyMethodDef functions[] = {
{"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, 1},
#endif
{"tiff_lzw_decoder", (PyCFunction)PyImaging_TiffLzwDecoderNew, 1},
#ifdef HAVE_LIBTIFF
{"tiff_ccitt_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1},
{"group3_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1},
{"group4_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1},
{"tiff_raw_16_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1},

{"tiff_ccitt_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1},
{"group3_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1},
{"group4_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1},
{"tiff_raw_16_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1},
#endif
{"msp_decoder", (PyCFunction)PyImaging_MspDecoderNew, 1},
{"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, 1},
{"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, 1},
Expand Down
Loading