From 92c7337df0b45d666270c292f2d4d41a1b3dc4f5 Mon Sep 17 00:00:00 2001 From: Marcus Brinkmann Date: Thu, 29 Sep 2016 03:16:04 +0200 Subject: [PATCH] Add TIFF save_all writer. --- PIL/Image.py | 4 +- PIL/TiffImagePlugin.py | 262 +++++++++++++++++++++++++++++++++++++++- Tests/test_file_tiff.py | 11 ++ 3 files changed, 275 insertions(+), 2 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 2f68e88b5..5a9ef480c 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -1686,7 +1686,9 @@ class Image(object): save_handler = SAVE[format.upper()] if open_fp: - fp = builtins.open(filename, "wb") + # Open also for reading ("+"), because TIFF save_all + # writer needs to go back and edit the written data. + fp = builtins.open(filename, "w+b") try: save_handler(self, fp, filename) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 7e073b49d..13b695410 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -1404,7 +1404,6 @@ def _save(im, fp, filename): if im.mode == "P": lut = im.im.getpalette("RGB", "RGB;L") ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut) - # data orientation stride = len(bits) * ((im.size[0]*bits[0]+7)//8) ifd[ROWSPERSTRIP] = im.size[1] @@ -1491,12 +1490,273 @@ def _save(im, fp, filename): # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd +class AppendingTiffWriter: + fieldSizes = [ + 0, # None + 1, # byte + 1, # ascii + 2, # short + 4, # long + 8, # rational + 1, # sbyte + 1, # undefined + 2, # sshort + 4, # slong + 8, # srational + 4, # float + 8, # double + ] + + # StripOffsets = 273 + # FreeOffsets = 288 + # TileOffsets = 324 + # JPEGQTables = 519 + # JPEGDCTables = 520 + # JPEGACTables = 521 + Tags = set((273, 288, 324, 519, 520, 521)) + + def __init__(self, fn, new=False): + if hasattr(fn, 'read'): + self.f = fn + self.close_fp = False + else: + self.name = fn + self.close_fp = True + try: + self.f = io.open(fn, "w+b" if new else "r+b") + except IOError: + self.f = io.open(fn, "w+b") + self.beginning = self.f.tell() + self.setup() + + def setup(self): + # Reset everything. + self.f.seek(self.beginning, os.SEEK_SET) + + self.whereToWriteNewIFDOffset = None + self.offsetOfNewPage = 0 + + self.IIMM = IIMM = self.f.read(4) + if not IIMM: + # empty file - first page + self.isFirst = True + return + + self.isFirst = False + if IIMM == b"II\x2a\x00": + self.setEndian("<") + elif IIMM == b"MM\x00\x2a": + self.setEndian(">") + else: + raise RuntimeError("Invalid TIFF file header") + + self.skipIFDs() + self.goToEnd() + + def finalize(self): + if self.isFirst: + return + + # fix offsets + self.f.seek(self.offsetOfNewPage) + + IIMM = self.f.read(4) + if not IIMM: + # raise RuntimeError("nothing written into new page") + # Make it easy to finish a frame without committing to a new one. + return + + if IIMM != self.IIMM: + raise RuntimeError("IIMM of new page doesn't match IIMM of " + "first page") + + IFDoffset = self.readLong() + IFDoffset += self.offsetOfNewPage + self.f.seek(self.whereToWriteNewIFDOffset) + self.writeLong(IFDoffset) + self.f.seek(IFDoffset) + self.fixIFD() + + def newFrame(self): + # Call this to finish a frame. + self.finalize() + self.setup() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if self.close_fp: + self.close() + return False + + def tell(self): + return self.f.tell() - self.offsetOfNewPage + + def seek(self, offset, whence): + if whence == os.SEEK_SET: + offset += self.offsetOfNewPage + + self.f.seek(offset, whence) + return self.tell() + + def goToEnd(self): + self.f.seek(0, os.SEEK_END) + pos = self.f.tell() + + # pad to 16 byte boundary + padBytes = 16 - pos % 16 + if 0 < padBytes < 16: + self.f.write(bytes(bytearray(padBytes))) + self.offsetOfNewPage = self.f.tell() + + def setEndian(self, endian): + self.endian = endian + self.longFmt = self.endian + "L" + self.shortFmt = self.endian + "H" + self.tagFormat = self.endian + "HHL" + + def skipIFDs(self): + while True: + IFDoffset = self.readLong() + if IFDoffset == 0: + self.whereToWriteNewIFDOffset = self.f.tell() - 4 + break + + self.f.seek(IFDoffset) + numTags = self.readShort() + self.f.seek(numTags * 12, os.SEEK_CUR) + + def write(self, data): + return self.f.write(data) + + def readShort(self): + value, = struct.unpack(self.shortFmt, self.f.read(2)) + return value + + def readLong(self): + value, = struct.unpack(self.longFmt, self.f.read(4)) + return value + + def rewriteLastShortToLong(self, value): + self.f.seek(-2, os.SEEK_CUR) + bytesWritten = self.f.write(struct.pack(self.longFmt, value)) + if bytesWritten is not None and bytesWritten != 4: + raise RuntimeError("wrote only %u bytes but wanted 4" % + bytesWritten) + + def rewriteLastShort(self, value): + self.f.seek(-2, os.SEEK_CUR) + bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) + if bytesWritten is not None and bytesWritten != 2: + raise RuntimeError("wrote only %u bytes but wanted 2" % + bytesWritten) + + def rewriteLastLong(self, value): + self.f.seek(-4, os.SEEK_CUR) + bytesWritten = self.f.write(struct.pack(self.longFmt, value)) + if bytesWritten is not None and bytesWritten != 4: + raise RuntimeError("wrote only %u bytes but wanted 4" % + bytesWritten) + + def writeShort(self, value): + bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) + if bytesWritten is not None and bytesWritten != 2: + raise RuntimeError("wrote only %u bytes but wanted 2" % + bytesWritten) + + def writeLong(self, value): + bytesWritten = self.f.write(struct.pack(self.longFmt, value)) + if bytesWritten is not None and bytesWritten != 4: + raise RuntimeError("wrote only %u bytes but wanted 4" % + bytesWritten) + + def close(self): + self.finalize() + self.f.close() + + def fixIFD(self): + numTags = self.readShort() + #trace("fixing IFD at %X; number of tags: %u (0x%X)", self.f.tell()-2, + # numTags, numTags) + + for i in range(numTags): + tag, fieldType, count = struct.unpack(self.tagFormat, self.f.read(8)) + #trace(" at %X: tag %u (0x%X), type %u, count %u", self.f.tell()-8, + # tag, tag, fieldType, count) + + fieldSize = self.fieldSizes[fieldType] + totalSize = fieldSize * count + isLocal = (totalSize <= 4) + if not isLocal: + offset = self.readLong() + offset += self.offsetOfNewPage + self.rewriteLastLong(offset) + + if tag in self.Tags: + curPos = self.f.tell() + + if isLocal: + self.fixOffsets(count, isShort=(fieldSize == 2), + isLong=(fieldSize == 4)) + self.f.seek(curPos + 4) + else: + self.f.seek(offset) + self.fixOffsets(count, isShort=(fieldSize == 2), + isLong=(fieldSize == 4)) + self.f.seek(curPos) + + offset = curPos = None + + elif isLocal: + # skip the locally stored value that is not an offset + self.f.seek(4, os.SEEK_CUR) + + def fixOffsets(self, count, isShort=False, isLong=False): + if not isShort and not isLong: + raise RuntimeError("offset is neither short nor long") + + for i in range(count): + offset = self.readShort() if isShort else self.readLong() + offset += self.offsetOfNewPage + if isShort and offset >= 65536: + # offset is now too large - we must convert shorts to longs + if count != 1: + raise RuntimeError("not implemented") # XXX TODO + + # simple case - the offset is just one and therefore it is + # local (not referenced with another offset) + self.rewriteLastShortToLong(offset) + self.f.seek(-10, os.SEEK_CUR) + self.writeShort(4) # rewrite the type to LONG + self.f.seek(8, os.SEEK_CUR) + elif isShort: + self.rewriteLastShort(offset) + else: + self.rewriteLastLong(offset) + +def _save_all(im, fp, filename): + if not hasattr(im, "n_frames"): + return _save(im, fp, filename) + + cur_idx = im.tell() + try: + with AppendingTiffWriter(fp) as tf: + for idx in range(im.n_frames): + im.seek(idx) + im.load() + _save(im, tf, filename) + tf.newFrame() + finally: + im.seek(cur_idx) + # # -------------------------------------------------------------------- # Register Image.register_open(TiffImageFile.format, TiffImageFile, _accept) Image.register_save(TiffImageFile.format, _save) +Image.register_save_all(TiffImageFile.format, _save_all) Image.register_extension(TiffImageFile.format, ".tif") Image.register_extension(TiffImageFile.format, ".tiff") diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 5feea07d7..45fdc2051 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -487,6 +487,17 @@ class TestFileTiff(PillowTestCase): self.assert_image_equal(im, reloaded) + def test_tiff_save_all(self): + import io + import os + + mp = io.BytesIO() + with Image.open("Tests/images/multipage.tiff") as im: + im.save(mp, format="tiff", save_all=True) + + mp.seek(0, os.SEEK_SET) + with Image.open(mp) as im: + self.assertEqual(im.n_frames, 3) if __name__ == '__main__': unittest.main()