Module ctsimu.tiffy

TIFF image reader and writer.

Functions

def TIFFtypeStructCharacter(tp)
def bytesPerTIFFtype(tp)
def getByteOrder(numpyArray)
def tagName(intTag)
def tiffyLog(level, message)

Classes

class bits
Expand source code
class bits:
    def __init__(self):
        self.data = bytearray()

    def set(self, n):
        """ Takes a number n and stores it in self.data in bitwise manner. """
        self.data = bytearray()

        pos = 0
        while pos>0:
            self.setBit(pos, n&1)
            n = n >> 1
            pos += 1

    def __str__(self):
        """ Print in binary representation. """
        maxPos = len(self.data)*8
        binString = ""

        pos = 0
        nChunks = 0
        nBytes = 0
        while pos <= maxPos:
            if pos%9 == 0:
                binString = "." + binString
                nChunks += 1
            if pos%8 == 0:
                binString = "|" + binString
                nBytes += 1

            binString = "{}".format(self.getBit(pos)) + binString
            pos += 1

        binString += "\n{} chunks in {} bytes.".format(nChunks, nBytes)
        return binString

    def setBytes(self, b):
        """ Takes a bytes object b and stores it in self.data. """
        self.data = bytearray()
        self.data += b

    def reverseBitsInBytes(self):
        for i in range(len(self.data)):
            newByte = 0
            for b in range(8):
                newByte = newByte << 1
                newByte += ((self.data[i] >> b) & 1)
                
            self.data[i] = newByte

    """
    def reverseBits(self, start, stop):
        newBits = bits()
        pos = 0
        for i in range(stopBit-1, startBit-1, -1):
            newBits.setBit(pos, self.getBit(i))
            pos += 1

        for i in range(startBit, stopBit):
            self.setBit(i, self.getBit(i-startBit))
    """

    def byteAndBit(self, pos):
        """ From a given bit position, returns the byte index and the bit index within this byte. """
        byteIdx = int(math.floor(pos/8))
        inByte = pos%8

        return byteIdx, inByte

    def getInt(self, startBit, stopBit):
        s = 0
        if startBit < stopBit:
            for i in range(stopBit-1, startBit-1, -1):
                s = s << 1
                s += self.getBit(i)

        return int(s)

    def getIntMSBtoLSB(self, startBit, stopBit):
        s = 0
        for i in range(startBit, stopBit):  # MSB to LSB
            s = s << 1
            s += self.getBit(i)

        return s

    def getIntMSBtoLSB_inBytes(self, startByte, startBit, nBits):
        s = 0

        # Maximum of three masks required (for 9..12 bits)
        mask1 = mask << startBit
        mask2 = 255 << (nBits-7)
        mask3 = 255

        """
        i = 0
        #print("StartByte: {}, StartBit: {}, nBits: {}".format(startByte, startBit, nBits))
        while i<nBits:  # MSB to LSB
            s = s << 1  # Shift left by 1
            s += ((self.data[startByte] >> (7-startBit)) & 1)

            startBit += 1
            if startBit > 7:
                startBit = 0
                startByte += 1

            i += 1
        """

        startBit += nBits
        if startBit > 7:
            startBit = 0
            startByte += 1

        return s, startByte+1, startBit+nBits

    def getIntMSBtoLSB_faster(self, startByte, rightZeros, mask0, mask1, mask2):
        s = ((self.data[startByte]&mask0)<<16) + ((self.data[startByte+1]&mask1)<<8) + (self.data[startByte+2]&mask2)

        s = s >> rightZeros
        #newByte = 0
        #for b in range(9):
        #    newByte = newByte << 1
        #    newByte += ((s >> b) & 1)

        #return newByte

        return s

    def getBit(self, pos):
        byteIdx, inByte = self.byteAndBit(pos)

        if(byteIdx >= len(self.data)):
            return 0

        return ((self.data[byteIdx] >> inByte) & 1)

    def getBitInByte(self, byteIdx, bitPos):
        return ((self.data[byteIdx] >> bitPos) & 1)

    def setBit(self, pos, value=1):
        byteIdx, inByte = self.byteAndBit(pos)

        while(byteIdx > len(self.data)):
            self.data += bytes(1)

        mask = 1 << inByte
        if value == 1:   # set bit
            self.data[byteIdx] = self.data[byteIdx] | mask
        else:  # clear bit
            self.data[byteIdx] = self.data[byteIdx] & ~mask

    def setBitInByte(self, byteIdx, bitPos, value=1):
        while(byteIdx > len(self.data)):
            self.data += bytes(1)

        mask = 1 << bitPos
        if value == 1:   # set bit
            self.data[byteIdx] = self.data[byteIdx] | mask
        else:  # clear bit
            self.data[byteIdx] = self.data[byteIdx] & ~mask       

Methods

def byteAndBit(self, pos)

From a given bit position, returns the byte index and the bit index within this byte.

def getBit(self, pos)
def getBitInByte(self, byteIdx, bitPos)
def getInt(self, startBit, stopBit)
def getIntMSBtoLSB(self, startBit, stopBit)
def getIntMSBtoLSB_faster(self, startByte, rightZeros, mask0, mask1, mask2)
def getIntMSBtoLSB_inBytes(self, startByte, startBit, nBits)
def reverseBitsInBytes(self)
def set(self, n)

Takes a number n and stores it in self.data in bitwise manner.

def setBit(self, pos, value=1)
def setBitInByte(self, byteIdx, bitPos, value=1)
def setBytes(self, b)

Takes a bytes object b and stores it in self.data.

class ifd
Expand source code
class ifd:
    def __init__(self):
        self.ifdPos = None
        self.fieldBytes = []
        self.fields = []
        self.fieldCount = 0
        self.nextIfdPos = 0

    def addEntry(self, entry):
        self.fields.append(entry)

    def read(self, ifdPos, f, byteOrder):
        self.ifdPos = ifdPos

        tiffyLog(TIFFY_LOGLEVEL_INFO, " -- new IFD.")

        f.seek(ifdPos)
        buff = f.read(2)
        offset = ifdPos + 2
        (self.fieldCount,) = struct.unpack("{endian}H".format(endian=byteOrder), buff)
        
        for i in range(self.fieldCount):
            entry = ifdEntry()
            entry.read(offset, f, byteOrder)
            self.fields.append(entry)
            offset += 12

        f.seek(offset)
        buff = f.read(4)
        (self.nextIfdPos,) = struct.unpack("{endian}L".format(endian=byteOrder), buff)

    def sizeInBytes(self):
        size = 2 + 4  # Number of entries (2 bytes) + offset to next IFD (4 bytes)
        for field in self.fields:
            size += field.sizeInBytes()

        return size

    def prepareDataOffsets(self, offset):
        self.ifdPos = offset

        offset += 2  # number of entries (2 bytes)
        extraDataOffset = offset + 12*len(self.fields) + 4  # offset for each entry (12 bytes) + pointer to next IFD (4 bytes)

        for field in self.fields:
            offset, extraDataOffset = field.prepareDataOffsets(offset, extraDataOffset)

        return offset

    def printOffset(self):
        tiffyLog(TIFFY_LOGLEVEL_INFO, "+ IFD                   {offset} -> {nextIFD}".format(offset=self.ifdPos, nextIFD=self.nextIfdPos))

        for entry in self.fields:
            entry.printOffset()

        for entry in self.fields:
            entry.printExtraDataOffset()

    def write(self, f, byteOrder):
        buff = struct.pack("{endian}H".format(endian=byteOrder), len(self.fields))
        f.write(buff)

        tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Written IFD. Now at position {}".format(f.tell()))

        # IFD entries:
        for entry in self.fields:
            entry.write(f, byteOrder)

        buff = struct.pack("{endian}L".format(endian=byteOrder), self.nextIfdPos)
        f.write(buff)

        # extra data
        for entry in self.fields:
            entry.writeExtraValues(f, byteOrder)

Methods

def addEntry(self, entry)
def prepareDataOffsets(self, offset)
def printOffset(self)
def read(self, ifdPos, f, byteOrder)
def sizeInBytes(self)
def write(self, f, byteOrder)
class ifdEntry
Expand source code
class ifdEntry:
    def __init__(self):
        self.tag      = 0  # TIFF Tag ID
        self.type     = 0  # Field Type
        self.count    = 0  # Number of values (NOT bytes)
        self.ifdEntryPos = 0

        self.values = []
        self.extraValuesOffset = 0   # storage offset (in byte) for any extra values that don't fit in the 4 values bytes of this IFD entry

    def set(self, tag, typ, values):
        self.setTagID(tag)
        self.setType(typ)
        self.setValues(values)

    def setTagID(self, tagid):
        self.tag = tagid

    def setType(self, typ):
        self.type = typ

    def setValue(self, value):
        if self.type in TIFF_INTTYPES:
            value = int(value)

        self.values = [value, ]
        self.count = 1

    def setValues(self, values):
        self.values = [0] * len(values)
     
        for i in range(len(self.values)):
            if self.type in TIFF_INTTYPES:
                self.values[i] = int(values[i])
            else:
                self.values[i] = values[i]

        self.count = len(values)

    def nValueBytes(self):
        return self.count * bytesPerTIFFtype(self.type)

    def read(self, offset, f, byteOrder):
        f.seek(offset)
        buff = f.read(8)
        (self.tag, self.type, self.count) = struct.unpack("{endian}HHL".format(endian=byteOrder), buff)

        # Calculate amount of bytes necessary for value(s):
        self.nValueBytes = self.nValueBytes()

        # Read value(s):
        f.seek(offset+8)
        buff = f.read(4)

        if self.nValueBytes > 4:  # The field's actual value bytes must be read from the provided pointer.
            (valueOffset,) = struct.unpack("{endian}L".format(endian=byteOrder), buff)
            f.seek(valueOffset)
            buff = f.read(self.nValueBytes)
      
        # Prepare a struct pattern:
        structPattern = ""
        structCharacter = TIFFtypeStructCharacter(self.type)
        for i in range(self.count):
            structPattern += structCharacter

        # Fill rest of pattern with pad bytes:
        if self.nValueBytes < 4:
            for j in range(4 - self.nValueBytes):
                structPattern += "x"

        tup = struct.unpack("{endian}{pattern}".format(endian=byteOrder, pattern=structPattern), buff)

        if self.type == TIFF_RATIONAL or self.type == TIFF_SRATIONAL:
            for numerator, denominator in zip(tup, tup[1:]):
                val = 0
                if denominator != 0:
                    val = numerator / denominator

                self.values.append(val)
        else:
            for val in tup:
                self.values.append(val)

        if len(self.values) < 5:
            tiffyLog(TIFFY_LOGLEVEL_INFO, " -- Tag {tag} ({tagname}): {n} value(s) {val}".format(tag=self.tag, tagname=tagName(self.tag), n=len(self.values), val=self.values))
        else:
            tiffyLog(TIFFY_LOGLEVEL_INFO, " -- Tag {tag} ({tagname}): {n} value(s)".format(tag=self.tag, tagname=tagName(self.tag), n=len(self.values)))

    def getValue(self):
        if len(self.values) > 0:
            return self.values[0]
        else:
            raise Exception("TIFF field with tag {tag} does not come with any value.".format(tag=self.tag))

    def sizeInBytes(self):
        size = 12
        size += self.sizeOfExtraValues()

        return size

    def sizeOfExtraValues(self):
        size = 0

        # If the values don't fit in the IFD entry, they are stored somewhere else:
        if (bytesPerTIFFtype(self.type)*len(self.values)) > 4:
            size += bytesPerTIFFtype(self.type)*len(self.values)

        return size

    def printOffset(self):
        tiffyLog(TIFFY_LOGLEVEL_INFO, "    IFD entry, tag {t}  {offset}".format(t=self.tag, offset=self.ifdEntryPos))

    def printExtraDataOffset(self):
        if self.nValueBytes() > 4:
            tiffyLog(TIFFY_LOGLEVEL_INFO, "    IFD entry, tag {t}  {offset}  Extra Data".format(t=self.tag, offset=self.extraValuesOffset))

    def prepareDataOffsets(self, offset, extraDataOffset):
        self.ifdEntryPos = offset
        self.extraValuesOffset = extraDataOffset
        return (offset+12), (extraDataOffset + self.sizeOfExtraValues())

    def write(self, f, byteOrder):
        tiffyLog(TIFFY_LOGLEVEL_INFO, "IDF Entry at pos {}".format(f.tell()))
        buff = struct.pack("{endian}HHL".format(endian=byteOrder), self.tag, self.type, self.count)
        f.write(buff)

        if self.nValueBytes() > 4:  # point to value storage offset
            tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Packing for tag {tag} (type {typ}, count {cnt}): Pointing to {offset}".format(tag=self.tag, typ=self.type, cnt=self.count, endian=byteOrder, offset=self.extraValuesOffset))
            buff = struct.pack("{endian}L".format(endian=byteOrder), self.extraValuesOffset)
            f.write(buff)
        else:
            structChar = TIFFtypeStructCharacter(self.type)

            padding = 4 - self.nValueBytes()
            if padding > 0:
                tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Packing for tag {tag} (type {typ}, count {cnt}) at pos {pos}: {endian}{count}{structchar}{padding}x {vals}".format(tag=self.tag, typ=self.type, cnt=self.count, pos=f.tell(), endian=byteOrder, count=self.count, structchar=structChar, padding=padding, vals=self.values))
                buff = struct.pack("{endian}{count}{structchar}{padding}x".format(endian=byteOrder, count=self.count, structchar=structChar, padding=padding), *self.values)
                f.write(buff)
            else:
                tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Packing for tag {tag} (type {typ}, count {cnt}) at pos {pos}: {endian}{count}{structchar} {vals}".format(tag=self.tag, typ=self.type, cnt=self.count, pos=f.tell(), endian=byteOrder, count=self.count, structchar=structChar, vals=self.values))
                buff = struct.pack("{endian}{count}{structchar}".format(endian=byteOrder, count=self.count, structchar=structChar), *self.values)
                f.write(buff)

    def writeExtraValues(self, f, byteOrder):
        structChar = TIFFtypeStructCharacter(self.type)
        if self.nValueBytes() > 4:
            tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Packing for tag {tag} (type {typ}, count {cnt}) at pos {pos}: {endian}{count}{structchar} {vals}".format(tag=self.tag, typ=self.type, cnt=self.count, pos=f.tell(), endian=byteOrder, count=self.count, structchar=structChar, vals=self.values))

            buff = struct.pack("{endian}{count}{structchar}".format(endian=byteOrder, count=self.count, structchar=structChar), *self.values)
            f.write(buff)

Methods

def getValue(self)
def nValueBytes(self)
def prepareDataOffsets(self, offset, extraDataOffset)
def printExtraDataOffset(self)
def printOffset(self)
def read(self, offset, f, byteOrder)
def set(self, tag, typ, values)
def setTagID(self, tagid)
def setType(self, typ)
def setValue(self, value)
def setValues(self, values)
def sizeInBytes(self)
def sizeOfExtraValues(self)
def write(self, f, byteOrder)
def writeExtraValues(self, f, byteOrder)
class lzwData
Expand source code
class lzwData:
    def __init__(self):
        self.compressed = None
        self.umcompressed = None
        self.stringTable = lzwStringTable()

        self.currentBitPos = 0

        self.currentByteIdx = 0
        self.currentBitPosInByte  = 0  # in byte

        self.codeWidth = 9  # Starting with 9 bit wide codes

        self.masks = {} #[[[0]*3]*8]*14
        self.byteSkips = {} # [[0]*8]*14
        self.leftAlignOffsets = {}

        for codeWidth in range(9, 14):
            fundMask = 2**codeWidth - 1

            for shift in range(0, 8):   # Bitshift / startBit
                leftAlignOffset = 3*8 - codeWidth - shift

                mask = fundMask << leftAlignOffset
                mask0 = (mask >> 16) & 255  # 00000000 00000000 11111111
                mask1 = (mask >> 8) & 255   # 00000000 11111111 00000000
                mask2 = (mask) & 255        # 11111111 00000000 00000000

                self.masks[codeWidth,shift,0] = mask0
                self.masks[codeWidth,shift,1] = mask1
                self.masks[codeWidth,shift,2] = mask2

                self.byteSkips[codeWidth,shift] = int(math.ceil((codeWidth+shift+1)/8))-1
                self.leftAlignOffsets[codeWidth,shift] = leftAlignOffset

    def resetStringtable(self):
        self.stringTable.init()
        self.codeWidth = 9

    def setCompressed(self, compressed):
        self.compressed = bits()
        self.compressed.setBytes(compressed)
        self.compressed.data += bytes(3)   # for integer conversion

    def getNextCode(self):
        #s1 = self.currentBitPos
        #s2 = s1 + self.codeWidth

        #code = self.compressed.getIntMSBtoLSB(startBit=s1, stopBit=s2)
        #code, self.currentByteIdx, self.currentBitPosInByte = self.compressed.getIntMSBtoLSB_inBytes(startByte=self.currentByteIdx, startBit=self.currentBitPosInByte, nBits=self.codeWidth)

        startByte = self.currentByteIdx
        mask0=self.masks[self.codeWidth,self.currentBitPosInByte,0]
        mask1=self.masks[self.codeWidth,self.currentBitPosInByte,1]
        mask2=self.masks[self.codeWidth,self.currentBitPosInByte,2]

        code = self.compressed.getIntMSBtoLSB_faster(startByte=startByte, rightZeros=self.leftAlignOffsets[self.codeWidth,self.currentBitPosInByte], mask0=mask0, mask1=mask1, mask2=mask2)

        tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Requesting {}({}). Skip: {} CodeWidth: {} Offset: {}  Masks: {:08b}.{:08b}.{:08b} -> {}".format(startByte, self.currentBitPosInByte, self.byteSkips[self.codeWidth,self.currentBitPosInByte], self.codeWidth, self.currentBitPosInByte, mask0, mask1, mask2, code))

        self.currentByteIdx += self.byteSkips[self.codeWidth,self.currentBitPosInByte]
        self.currentBitPosInByte += self.codeWidth
        self.currentBitPosInByte = self.currentBitPosInByte % 8

        return code

    def decompress(self):
        self.uncompressed = bytearray()

        self.currentBitPos = 0
        oldCode = -1
        code = self.getNextCode()

        self.stringTable.init()  # If compressed data doesn't start with ClearCode

        while not self.stringTable.isEndOfInformation(code):
            if(self.stringTable.isClearCode(code)):
                self.resetStringtable()                

                code = self.getNextCode()
                if(self.stringTable.isEndOfInformation(code)):
                    break

                self.uncompressed += self.stringTable.stringFromCode(code, self.codeWidth)
                oldCode = code
            else:
                if self.stringTable.contains(code):
                    s = self.stringTable.stringFromCode(code, self.codeWidth)
                    self.uncompressed += s
                    self.stringTable.add(self.stringTable.stringFromCode(oldCode, self.codeWidth) + bytes([s[0]]))
                else:
                    s = self.stringTable.stringFromCode(oldCode, self.codeWidth)
                    outString = s + bytes([s[0]])
                    self.uncompressed += outString
                    self.stringTable.add(outString)

                oldCode = code

                self.codeWidth = self.stringTable.currentCodeBitWidth()

            code = self.getNextCode()

Methods

def decompress(self)
def getNextCode(self)
def resetStringtable(self)
def setCompressed(self, compressed)
class lzwStringTable
Expand source code
class lzwStringTable:
    def __init__(self):
        self.byteStrings = []
        self.init()

    def init(self):
        self.byteStrings = []

        # Create all bytes:
        for i in range(256):
            self.byteStrings.append(bytearray(struct.pack("B", i)))

        self.byteStrings.append(bytearray())  # ClearCode: 256
        self.byteStrings.append(bytearray())  # EndOfInformation code: 257

    def currentCodeBitWidth(self):
        """
        l = len(self.byteStrings) + 1
        bitWidth = 0
        while l != 0:
            l = l >> 1
            bitWidth += 1

        return bitWidth
        """

        if len(self.byteStrings) < 255:
            return 8
        elif len(self.byteStrings) < 511:
            return 9
        elif len(self.byteStrings) < 1023:
            return 10
        elif len(self.byteStrings) < 2047:
            return 11
        elif len(self.byteStrings) < 4095:
            return 12
        elif len(self.byteStrings) < 8191:
            return 13
        elif len(self.byteStrings) < 16383:
            return 14
        else:
            raise Exception("LZW: Dictionary is too big.")

    def contains(self, code):
        if code < len(self.byteStrings):
            return True

        return False

    def add(self, b):
        self.byteStrings.append(b)

    def isClearCode(self, code):
        if code == 256:
            return True

        return False

    def isEndOfInformation(self, code):
        if code == 257:
            return True

        return False

    def stringFromCode(self, code, codeWidth):
        #if code < 0:
        #    return bytearray()

        return self.byteStrings[code]

        """
        if code < len(self.byteStrings):
            #print("String from code {}: {}".format(code, self.byteStrings[code]))
            return self.byteStrings[code]
        else:
            raise Exception("LZW: Requested code #{} not in current string table. Current string table size: {}. Current bit width: {}".format(code, len(self.byteStrings), codeWidth))
        """

Methods

def add(self, b)
def contains(self, code)
def currentCodeBitWidth(self)

l = len(self.byteStrings) + 1 bitWidth = 0 while l != 0: l = l >> 1 bitWidth += 1

return bitWidth

def init(self)
def isClearCode(self, code)
def isEndOfInformation(self, code)
def stringFromCode(self, code, codeWidth)
class tiff
Expand source code
class tiff:
    def __init__(self):
        self.filename = None
        self.byteOrder = "<"
        self.subfiles = []

    def reset(self):
        self.__init__()

    def read(self, filename=None):
        self.filename = filename

        if self.filename is not None:
            if os.path.isfile(self.filename):
                filesize = os.path.getsize(self.filename)
                tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Size of {filename}: {size} Bytes.".format(filename=self.filename, size=filesize))

                if filesize > 2:
                    with open(self.filename, "rb") as f:
                        buff = f.read(2)
                        (tiffformat,) = struct.unpack("<H", buff)  # Just read as little endian. It's symmetric, it doesn't matter.

                        self.byteOrder = "<"   # little endian
                        if tiffformat == 0x4949:
                            tiffyLog(TIFFY_LOGLEVEL_DEBUG, "II format. Little endian.")
                            self.byteOrder = "<"
                        elif tiffformat == 0x4d4d:
                            tiffyLog(TIFFY_LOGLEVEL_DEBUG, "MM format. Big endian.")
                            self.byteOrder = ">"
                        else:
                            f.close()
                            raise Exception("Invalid byte order for first two bytes in TIFF header. Must be 0x4949 (II) or 0x4d4d (MM).")

                        buff = f.read(2)
                        (magicByte,) = struct.unpack("{endian}H".format(endian=self.byteOrder), buff)
                        if magicByte == 42:
                            tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Magic Byte: {}".format(magicByte))
                        else:
                            raise Exception("TIFF magic byte is not 42: {}".format(self.filename))


                        # Get location of first image file directory (IFD):
                        buff = f.read(4)
                        (ifdPos,) = struct.unpack("{endian}L".format(endian=self.byteOrder), buff)

                        ifd0 = ifd()
                        ifd0.read(ifdPos, f, self.byteOrder)

                        self.subfiles = []
                        subfile0 = tiffSubfile()
                        subfile0.readMetaInformation(self.filename, self.byteOrder, ifd0)
                        self.subfiles.append(subfile0)

                        nextIfdPos = ifd0.nextIfdPos
                        while nextIfdPos != 0:
                            ifdn = ifd()
                            ifdn.read(nextIfdPos, f, self.byteOrder)
                            subfilen = tiffSubfile()
                            subfilen.readMetaInformation(self.filename, self.byteOrder, ifdn)
                            self.subfiles.append(subfilen)
                            nextIfdPos = ifdn.nextIfdPos

                        f.close()
                else:
                    raise Exception("TIFF file does not contain any data: {}".format(self.filename))
            else:
                raise Exception("File not found: {}".format(self.filename))

    def imageData(self, subfile=0, channel=None, obeyOrientation=True):
        if subfile < len(self.subfiles):
            data = self.subfiles[subfile].imageData()
            if channel is None:
                return data
            else:
                return data[0]
        else:
            raise Exception("Subfile id not available: {}".format(subfile))

    def getNrSubfiles(self):
        """ Return number of subfiles. """
        return len(self.subfiles)

    def getWidth(self, subfile=0):
        if subfile < len(self.subfiles):
            return self.subfiles[subfile].getWidth()

    def getHeight(self, subfile=0):
        if subfile < len(self.subfiles):
            return self.subfiles[subfile].getHeight()

    def sizeInBytes(self):
        size = 8  # TIFF header

        # Add size for each sub file:
        for sub in self.subfiles:
            size += sub.sizeInBytes()

        return size

    def prepareDataOffsets(self):
        offset = 8  # TIFF header
        for sub in self.subfiles:
            offset = sub.prepareDataOffsets(offset)

    def printOffset(self):
        self.prepareDataOffsets()
        tiffyLog(TIFFY_LOGLEVEL_INFO, "TIFF HEADER             0")
        for sub in self.subfiles:
            sub.printOffset()

    def set(self, imageData, resX=0, resY=0):
        self.reset()
        self.addImgData(imageData, resX, resY)

    def addImgData(self, imageData, resX=0, resY=0):
        if len(numpy.shape(imageData)) == 2:
            # We have to add another dimension for the channel...
            imageData = numpy.expand_dims(a=imageData, axis=0)

        if len(numpy.shape(imageData)) == 3:
            img = tiffSubfile()
            img.set(imageData, resX, resY)
            self.subfiles.append(img)

            # IFDs must be setup twice to ensure correct data offset pointers.
            for sub in self.subfiles:
                sub.setupIFD()
            self.prepareDataOffsets()
            for sub in self.subfiles:
                sub.setupIFD()
        else:
            raise Exception("TIFF: adding image data failed. Image data must be a numpy array of shape (height, width) or (channels, height, width).")

    def save(self, filename, endian="little"):
        byteOrder = "<" # little endian is default
        if endian == "big":
            byteOrder = ">"

        with open(filename, 'wb') as f:
            # Write the TIFF header.

            # Endian:
            if byteOrder == ">":
                buff = struct.pack("{endian}H".format(endian=byteOrder), 0x4d4d)
            else:
                buff = struct.pack("{endian}H".format(endian=byteOrder), 0x4949)
            f.write(buff)

            # Magic number:
            buff = struct.pack("{endian}H".format(endian=byteOrder), 42)
            f.write(buff)

            # The IFD0 always follows the header in our case:
            buff = struct.pack("{endian}L".format(endian=byteOrder), 8)
            f.write(buff)

            for sub in self.subfiles:
                sub.write(f, byteOrder)
           
            f.close()

Methods

def addImgData(self, imageData, resX=0, resY=0)
def getHeight(self, subfile=0)
def getNrSubfiles(self)

Return number of subfiles.

def getWidth(self, subfile=0)
def imageData(self, subfile=0, channel=None, obeyOrientation=True)
def prepareDataOffsets(self)
def printOffset(self)
def read(self, filename=None)
def reset(self)
def save(self, filename, endian='little')
def set(self, imageData, resX=0, resY=0)
def sizeInBytes(self)
class tiffSubfile
Expand source code
class tiffSubfile:
    def __init__(self):
        self.px = []      # Array of numpy arrays to store image data (i.e. pixel values). One for each component/channel. Shape: (nChannel, nRows, nCols)
        self.imageDataOffset = 0  # Location of beginning of image data
        self.sampleFormat = [TIFF_SAMPLEFORMAT_UINT]  # Standard: uint

        self.filename = None
        self.byteOrder = "<"  # little endian
        self.ifd = None

        self.photometricInterpretation = TIFF_BLACK_IS_ZERO
        self.compression = TIFF_NO_COMPRESSION
        self.predictor = TIFF_NO_PREDICTOR

        self.rows = 0
        self.cols = 0

        self.resolutionUnit = TIFF_RES_INCH
        self.resX = 0
        self.resY = 0

        self.rowsPerStrip = 0
        self.stripOffsets = []
        self.stripByteCounts = []

        self.bitsPerSample = (1,)        # Bilevel images do not define this, so make 1 bit/sample the default.
        self.samplesPerPixel = 1
        self.planarConfig = TIFF_CHUNKY  # Chunky (RGBRGBRGB...)
        self.orientation = 1
        self.colorMap = None             # For palette-color images

    def reset(self):
        self.__init__()

    def set(self, imageData, resX=0, resY=0):
        if len(imageData) > 0:
            self.reset()
            self.px = imageData

            shp = numpy.shape(self.px)  # Shape must be 3-component tuple: (nChannels, height, width)
            if len(shp) == 3:
                self.samplesPerPixel = shp[0]
                self.rows = shp[1]
                self.cols = shp[2]

                self.rowsPerStrip = self.rows
                # self.stripByteCounts and self.stripOffsets will be set later by self.prepareDataOffsets()

                # Byte order
                self.byteOrder = getByteOrder(self.px)

                # Sample format
                if numpy.issubdtype(self.px.dtype, numpy.signedinteger):
                    self.sampleFormat = [TIFF_SAMPLEFORMAT_INT] * self.nChannels()
                elif numpy.issubdtype(self.px.dtype, numpy.unsignedinteger):
                    self.sampleFormat = [TIFF_SAMPLEFORMAT_UINT] * self.nChannels()
                elif numpy.issubdtype(self.px.dtype, numpy.floating):
                    self.sampleFormat = [TIFF_SAMPLEFORMAT_IEEEFP] * self.nChannels()
                else:
                    raise Exception("Unsupported dtype ({}) of provided image data. Must be an integer or floating point type.".format(numpy.dtype(self.px)))

                self.bitsPerSample  = [self.px.dtype.itemsize*8] * self.samplesPerPixel
                tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Setting bits per sample: {}".format(self.bitsPerSample))
                tiffyLog(TIFFY_LOGLEVEL_DEBUG, "Setting sample format:   {}".format(self.sampleFormat))

                self.resolutionUnit = TIFF_RES_NONE
                self.resX = resX
                self.resY = resY

                self.compression    = TIFF_NO_COMPRESSION

                
                self.photometricInterpretation = TIFF_BLACK_IS_ZERO
                if self.nChannels() == 3:
                    self.photometricInterpretation = TIFF_RGB

                #if self.samplesPerPixel > 1:
                #    self.planarConfig   = TIFF_PLANAR
                #else:
                #    self.planarConfig   = TIFF_CHUNKY
                self.planarConfig   = TIFF_CHUNKY

            else:
                raise Exception("Error setting image data. Please provide a numpy array of shape (nChannels, nRows, nColumns).")

    def addIFDentry_shortOrLong(self, tag, values):
        # find the right integer type for the provided values:
        m = max(values)
        typ = TIFF_LONG
        if m < (2**16):
            typ = TIFF_SHORT

        self.addIFDentry(tag, typ, values)

    def addIFDentry(self, tag, typ, values):
        entry = ifdEntry()
        entry.set(tag, typ, values)
        self.ifd.addEntry(entry)

    def setupIFD(self):
        self.ifd = None
        self.ifd = ifd()

        self.addIFDentry_shortOrLong(256, (self.cols, ))                        # n columns
        self.addIFDentry_shortOrLong(257, (self.rows, ))                        # n rows
        self.addIFDentry(258, TIFF_SHORT, self.bitsPerSample)                   # bits per sample, already an array
        self.addIFDentry(259, TIFF_SHORT, (TIFF_NO_COMPRESSION, ))               # compression
        self.addIFDentry(262, TIFF_SHORT, (self.photometricInterpretation, ))   # photometric interpretation
        self.addIFDentry(266, TIFF_SHORT, (TIFF_MSB2LSB, ))                      # fill order 
        self.addIFDentry_shortOrLong(273, (self.imageDataOffset, ))             # offset location of strip
        self.addIFDentry(274, TIFF_SHORT, (self.orientation, ))                 # orientation
        self.addIFDentry(277, TIFF_SHORT, (self.samplesPerPixel, ))             # samples per pixel
        self.addIFDentry_shortOrLong(278, (self.rowsPerStrip, ))                # rows per strip
        self.addIFDentry_shortOrLong(279, (self.dataSizeInBytes(), ))            # strip byte counts
        # insert resolution entries here later...
        self.addIFDentry(284, TIFF_SHORT, (self.planarConfig, ))                # planar configuration
       
        #self.addIFDentry(296, TIFF_SHORT, (self.resolutionUnit, ))             # resolution unit
        
        if self.predictor != TIFF_NO_PREDICTOR:
            self.addIFDentry(317, TIFF_SHORT, (self.predictor, ))                   # predictor
        # color map
        
        self.addIFDentry(339, TIFF_SHORT, self.sampleFormat)                    # sample format, already an array

    def dataSizeInBytes(self):
        """ Size of pixel data (in bytes) """
        size = 0
        for bitsPerSample in self.bitsPerSample:
            s = int(bitsPerSample * self.nPixels())
            size += s

        return size/8

    def sizeInBytes(self):
        size = 0
        if self.ifd is not None:
            size += self.ifd.sizeInBytes()

        size += self.dataSizeInBytes()

        return size

    def prepareDataOffsets(self, offset):
        if self.ifd is not None:
            self.ifd.prepareDataOffsets(offset)

            offset += self.ifd.sizeInBytes()
            self.imageDataOffset = offset  # image data starts here

            self.stripOffsets    = (self.imageDataOffset, )
            self.stripByteCounts = self.dataSizeInBytes()

            offset += self.dataSizeInBytes()
            return offset
        else:
            return 0

    def printOffset(self):
        if self.ifd is not None:
            self.ifd.printOffset()

        tiffyLog(TIFFY_LOGLEVEL_INFO, "  Image Data            {offset}".format(offset=self.imageDataOffset))

    def readMetaInformation(self, filename, byteOrder, imgFileDirectory):
        self.filename = filename
        self.byteOrder = byteOrder
        self.ifd = imgFileDirectory

        # Interpret IFD fields based on their TIFF tags:
        for field in self.ifd.fields:
            if field.tag == 256:  # Number of columns
                self.cols = field.getValue()
            elif field.tag == 257:  # Number of rows
                self.rows = field.getValue()
            elif field.tag == 258:  # Bits per sample
                self.bitsPerSample = field.values
            elif field.tag == 259:  # Compression
                self.compression = field.getValue()
                if self.compression == 0:
                    self.compression = TIFF_NO_COMPRESSION
            elif field.tag == 262:    # Photometric interpretation
                self.photometricInterpretation = field.getValue()
            elif field.tag == 273:  # Strip offsets
                self.stripOffsets = field.values
            elif field.tag == 274:  # Orientation
                self.orientation = field.getValue()
            elif field.tag == 277:  # Samples per pixel
                self.samplesPerPixel = field.getValue()
            elif field.tag == 278:  # Rows per strip
                self.rowsPerStrip = field.getValue()
            elif field.tag == 279:  # Strip byte counts
                self.stripByteCounts = field.values
            elif field.tag == 282:  # x resolution
                self.resX = field.getValue()
            elif field.tag == 283:  # y resolution
                self.resY = field.getValue()
            elif field.tag == 284:  # Planar configuration (order of pixel components)
                self.planarConfig = field.getValue()
            elif field.tag == 296:  # Resolution unit
                self.resolutionUnit = field.getValue()
            elif field.tag == 317:  # Predictor
                self.predictor = field.getValue()
            elif field.tag == 320:  # Color map
                pass # implement later...
            elif field.tag == 339:  # Sample format
                self.sampleFormat = field.values            

    def nChannels(self):
        return self.samplesPerPixel

    def nPixels(self):
        return self.rows*self.cols

    def bitsPerPixel(self):
        bits = 0
        for b in self.bitsPerSample:
            bits += b

        return bits

    def isPlanar(self):
        return (self.samplesPerPixel == 1 or self.planarConfig == TIFF_PLANAR)

    def isSet(self):
        """ Check if image has a valid width and height. """
        if(self.getHeight() > 0):
            if(self.getWidth() > 0):
                return True

        return False

    def getWidth(self):
        return self.cols

    def getHeight(self):
        return self.rows

    def rot90(self):
        if self.isSet():
            self.px = numpy.rot90(self.px, k=1, axes=(1,2))
            self.cols, self.rows = self.rows, self.cols

    def rot180(self):
        if self.isSet():
            self.px = numpy.rot90(self.px, k=2, axes=(1,2))

    def rot270(self):
        if self.isSet():
            self.px = numpy.rot90(self.px, k=-1, axes=(1,2))
            self.cols, self.rows = self.rows, self.cols

    def rotate(self, rotation):
        if rotation == "90":
            self.rot90()
        elif rotation == "180":
            self.rot180()
        elif rotation == "270":
            self.rot270()

    def flipHorizontal(self):
        if self.isSet():
            for i in range(self.nChannels()):
                self.px[i] = numpy.fliplr(self.px[i])

    def flipVertical(self):
        if self.isSet():
            for i in range(self.nChannels()):
                self.px[i] = numpy.flipud(self.px[i])

    def setFlip(self, horz=False, vert=False):
        self.flipHorz = horz
        self.flipVert = vert

    def getHorizontalFlip(self):
        return self.flipHorz

    def getVerticalFlip(self):
        return self.flipVert

    def flip(self, horizontal=False, vertical=False):
        if horizontal:
            self.flipHorizontal()
        if vertical:
            self.flipVertical()

    def numpyDatatype(self, sampleFormat, bitsPerSample):
        formatString = self.byteOrder
        if sampleFormat == TIFF_SAMPLEFORMAT_UINT:  # unsigned
            if bitsPerSample == 8:
                formatString = "u1"   # unsigned char (1 byte)
            elif bitsPerSample == 16:
                formatString += "u2"   # unsigned short (2 bytes)
            elif bitsPerSample == 32:
                formatString += "u4"   # unsigned long (4 bytes)
            elif bitsPerSample == 64:
                formatString += "u8"   # unsigned long long (8 bytes)
        elif sampleFormat == TIFF_SAMPLEFORMAT_INT:  # signed
            if bitsPerSample == 8:
                formatString = "i1"   # signed char (1 byte)
            elif bitsPerSample == 16:
                formatString += "i2"   # signed short (2 bytes)
            elif bitsPerSample == 32:
                formatString += "i4"   # signed long (4 bytes)
            elif bitsPerSample == 64:
                formatString += "i8"   # signed long long (8 bytes)
        elif sampleFormat == TIFF_SAMPLEFORMAT_IEEEFP:
            if bitsPerSample == 16:
                formatString += "f2"   # float 16 bit
            elif bitsPerSample == 32:
                formatString += "f4"   # float 32 bit
            elif bitsPerSample == 64:
                formatString += "f8"   # double 64 bit

        if len(formatString) >= 1:
            return numpy.dtype(formatString)

        raise Exception("Unsupported data type: {bps} bits per sample for TIFF sample format {sf}.".format(bps=bitsPerSample, sf=sampleFormat))

    def structDataTypeString(self, sampleFormat, bitsPerSample):
        formatString = self.byteOrder
        if sampleFormat == TIFF_SAMPLEFORMAT_UINT:  # unsigned
            if bitsPerSample == 8:
                formatString = "B"   # unsigned char (1 byte)
            elif bitsPerSample == 16:
                formatString += "H"   # unsigned short (2 bytes)
            elif bitsPerSample == 32:
                formatString += "L"   # unsigned long (4 bytes)
            elif bitsPerSample == 64:
                formatString += "Q"   # unsigned long long (8 bytes)
        elif sampleFormat == TIFF_SAMPLEFORMAT_INT:  # signed
            if bitsPerSample == 8:
                formatString = "b"   # signed char (1 byte)
            elif bitsPerSample == 16:
                formatString += "h"   # signed short (2 bytes)
            elif bitsPerSample == 32:
                formatString += "l"   # signed long (4 bytes)
            elif bitsPerSample == 64:
                formatString += "q"   # signed long long (8 bytes)
        elif sampleFormat == TIFF_SAMPLEFORMAT_IEEEFP:
            if bitsPerSample == 16:
                formatString += "e"   # float 16 bit
            elif bitsPerSample == 32:
                formatString += "f"   # float 32 bit
            elif bitsPerSample == 64:
                formatString += "d"   # double 64 bit

        if len(formatString) >= 1:
            return formatString

        raise Exception("Unsupported data type: {bps} bits per sample for TIFF sample format {sf}.".format(bps=bitsPerSample, sf=sampleFormat))

    def importFromUncompressedBuffer(self, pixelOffset, nPixels, datatype, channel, buff):
        if self.isPlanar():  # Buffer contains data just for one channel.
            self.px[channel][int(pixelOffset):int(pixelOffset+nPixels)] = numpy.frombuffer(buff, dtype=datatype)
        else: # CHUNKY configuration. channel is irrelevant here (and wrong.)
            bitsPerPixel = self.bitsPerPixel()
            bytesPerPixel = int(bitsPerPixel / 8)

            bytesPerSample = [x/8 for x in self.bitsPerSample]

            buff1d = numpy.frombuffer(buff, dtype=datatype)
            self.px[int(pixelOffset):int(pixelOffset+nPixels)] = numpy.reshape(buff1d, (nPixels, self.nChannels()))

            """
            for pixel in range(nPixels):
                channelOffset = 0

                

                for c in range(self.samplesPerPixel):
                    # Current index in bytes
                    idxStart = int((pixel + pixelOffset)*bytesPerPixel + channelOffset/8)
                    idxStop  = int(idxStart + bytesPerSample[c])

                    #print("Start: {}, Stop: {}".format(idxStart, idxStop))

                    self.px[c][pixel] = numpy.frombuffer(buff[idxStart:idxStop], dtype=dt)

                    channelOffset += self.bitsPerSample[c]
            """

    def imageData(self, obeyOrientation=True):
        # Read data into byte buffer:
        if len(self.stripOffsets) > 0:
            if len(self.stripOffsets) == len(self.stripByteCounts):
                if os.path.isfile(self.filename):
                    with open(self.filename, "rb") as f:
                        # Initialize nChannels x nPixels array for each channel:

                        # Check if bits per sample is the same for each channel:
                        if self.bitsPerSample.count(self.bitsPerSample[0]) == len(self.bitsPerSample) and self.sampleFormat.count(self.sampleFormat[0]) == len(self.sampleFormat):
                            sampleFormat  = self.sampleFormat[0]
                            bitsPerSample = self.bitsPerSample[0]

                            dt = self.numpyDatatype(sampleFormat, bitsPerSample)

                            if self.isPlanar():
                                self.px = numpy.zeros((self.nChannels(), self.nPixels()), dtype=dt)
                            else:
                                # Make array the shape of a chunky tiff configuration and reshape later...
                                self.px = numpy.zeros((self.nPixels(), self.nChannels()), dtype=dt)

                            nStrips = len(self.stripOffsets)

                            if self.compression == TIFF_NO_COMPRESSION or self.compression == TIFF_LZW_COMPRESSION:
                                pixel = 0
                                bitsPerPixel = self.bitsPerPixel()

                                c = 0  # channel id. Only necessary for PLANAR configuration.
                                nStripsPerChannel = int(nStrips / self.samplesPerPixel)

                                for i in range(nStrips):
                                    if self.samplesPerPixel > 1:
                                        if self.planarConfig == TIFF_PLANAR:
                                            # Import next channel once all strips for one component channel have been imported.
                                            if (i % nStripsPerChannel) == 0:
                                                c += 1

                                    offset = self.stripOffsets[i]
                                    byteCount = self.stripByteCounts[i]  # byte count after compression

                                    #print("Strip #{}/{}, Byte Offset: {}, Byte Count: {}".format(i, nStrips, offset, byteCount))

                                    f.seek(offset)
                                    buff = f.read(byteCount)
                                    if self.compression == TIFF_LZW_COMPRESSION:
                                        compressed = lzwData()
                                        compressed.setCompressed(buff)

                                        #print("Decompressing LZW for strip {i}/{n}...".format(i=i, n=nStrips))
                                        compressed.decompress()
                                        buff = bytes(compressed.uncompressed)

                                    byteCount = len(buff)
                                    bitCount = 8*byteCount
                                    if not self.isPlanar(): # Standard is chunky, also for only 1 sample/pixel.
                                        pixelsInStrip = int(bitCount / bitsPerPixel)
                                    else:
                                        pixelsInStrip = int(bitCount / self.bitsPerSample[c])

                                    self.importFromUncompressedBuffer(
                                        pixelOffset = pixel,
                                        nPixels = pixelsInStrip,
                                        datatype = dt,
                                        channel = c,
                                        buff = buff)

                                    pixel += pixelsInStrip

                                tiffyLog(TIFFY_LOGLEVEL_INFO, "{} strips imported.".format(nStrips))
                            else:
                                raise Exception("TIFF: Compression scheme {} not supported.".format(self.compression))

                            f.close()

                            if not(self.isPlanar()):
                                # Chunky mode. Swap axes from (pixels, channels) to (channels, pixels):
                                self.px = numpy.swapaxes(self.px, 0, 1)

                            # Reshape components into 2D arrays:
                            if len(self.px) > 0:
                                self.px = numpy.reshape(self.px, (self.nChannels(), self.rows, self.cols))

                            # Convert horizontal differences to absolute values:
                            tiffyLog(TIFFY_LOGLEVEL_INFO, "Applying horizontal differencing...")
                            if self.predictor == TIFF_HORIZONTAL_DIFFERENCING:
                                for col in range(1, self.cols):
                                    self.px[...,col] = self.px[...,col-1] + self.px[...,col]

                            tiffyLog(TIFFY_LOGLEVEL_INFO, "Orientation is: {}".format(self.orientation))
                            if obeyOrientation:
                                # Rotate back to orientation 1 ((0,0) is upper left)
                                if self.orientation == 2:     # (col0, row0) is (top, right)
                                    self.flip(horizontal=True, vertical=False)
                                elif self.orientation == 3:   # (col0, row0) is (bottom, right)
                                    self.flip(horizontal=True, vertical=True)
                                elif self.orientation == 4:   # (col0, row0) is bottom left
                                    self.flip(horizontal=False, vertical=True)
                                elif self.orientation == 5:   # (col0, row0) is (left, top)
                                    self.rotate("90")
                                    self.flip(horizontal=False, vertical=True)
                                elif self.orientation == 6:   # (col0, row0) is (right, top)
                                    self.rotate("270")
                                elif self.orientation == 7:   # (col0, row0) is (right, bottom)
                                    self.rotate("90")
                                    self.flip(horizontal=True, vertical=False)
                                elif self.orientation == 8:   # (col0, row0) is (left, bottom)
                                    self.rotate("90")

                                self.orientation = 1

                            return self.px
                        else:
                            f.close()
                            raise Exception("Unsupported TIFF format: all channels must have the same type and size. Sample format: {}, Bits per sample: {}".format(self.sampleFormat, self.bitsPerSample))                       

                raise Exception("File not available: {}".format(self.filename))
            else:
                raise Exception("Number of strip offsets ({nOffsets}) does not match number of strip byte counts ({nByteCounts}).".format(nOffsets=len(self.stripOffsets), nByteCounts=len(self.stripByteCounts)))
        else:
            raise Exception("No data strips found for requested subfile in {filename}.".format(filename=self.filename))

    def write(self, f, byteOrder):
        """ Expects an open, writable file pointer f. """
        self.ifd.write(f, byteOrder)

        dataByteOrder = getByteOrder(self.px)
        if dataByteOrder != byteOrder:
            self.px.byteswap(inplace=True)

        # Write image data:
        if self.samplesPerPixel == 1:
            f.write(self.px)
        else:
            chunkyBytes = numpy.swapaxes(self.px, 0, 2)  # channel <-> cols  --> (cols, rows, channel)
            chunkyBytes = numpy.swapaxes(chunkyBytes, 0, 1)  # cols <-> rows  --> (rows, cols, channel)
            chunkyBytes.tofile(f, "")

            """
            dataString = []
            for c in range(self.nChannels()):
                dataString.append(self.structDataTypeString(self.sampleFormat[c], self.bitsPerSample[c]))

            for y in range(self.rows):
                for x in range(self.cols):
                    # Chunky style.
                    for c in range(self.nChannels()):
                        buff = struct.pack("{endian}{ds}".format(endian=byteOrder, ds=dataString[c]), self.px[c][y][x])
                        f.write(buff)
            """

Methods

def addIFDentry(self, tag, typ, values)
def addIFDentry_shortOrLong(self, tag, values)
def bitsPerPixel(self)
def dataSizeInBytes(self)

Size of pixel data (in bytes)

def flip(self, horizontal=False, vertical=False)
def flipHorizontal(self)
def flipVertical(self)
def getHeight(self)
def getHorizontalFlip(self)
def getVerticalFlip(self)
def getWidth(self)
def imageData(self, obeyOrientation=True)
def importFromUncompressedBuffer(self, pixelOffset, nPixels, datatype, channel, buff)
def isPlanar(self)
def isSet(self)

Check if image has a valid width and height.

def nChannels(self)
def nPixels(self)
def numpyDatatype(self, sampleFormat, bitsPerSample)
def prepareDataOffsets(self, offset)
def printOffset(self)
def readMetaInformation(self, filename, byteOrder, imgFileDirectory)
def reset(self)
def rot180(self)
def rot270(self)
def rot90(self)
def rotate(self, rotation)
def set(self, imageData, resX=0, resY=0)
def setFlip(self, horz=False, vert=False)
def setupIFD(self)
def sizeInBytes(self)
def structDataTypeString(self, sampleFormat, bitsPerSample)
def write(self, f, byteOrder)

Expects an open, writable file pointer f.