eggification
[Photo.git] / Products / Photo / exif.py
diff --git a/Products/Photo/exif.py b/Products/Photo/exif.py
new file mode 100755 (executable)
index 0000000..8120852
--- /dev/null
@@ -0,0 +1,374 @@
+# -*- coding: utf-8 -*-
+#######################################################################################
+#   Photo is a part of Plinn - http://plinn.org                                       #
+#   Copyright © 2008  Benoît PIN <benoit.pin@ensmp.fr>                                #
+#                                                                                     #
+#   This program is free software; you can redistribute it and/or                     #
+#   modify it under the terms of the GNU General Public License                       #
+#   as published by the Free Software Foundation; either version 2                    #
+#   of the License, or (at your option) any later version.                            #
+#                                                                                     #
+#   This program is distributed in the hope that it will be useful,                   #
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of                    #
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                     #
+#   GNU General Public License for more details.                                      #
+#                                                                                     #
+#   You should have received a copy of the GNU General Public License                 #
+#   along with this program; if not, write to the Free Software                       #
+#   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.   #
+#######################################################################################
+""" Exif version 2.2 read/write module.
+
+
+
+"""
+
+TYPES_SIZES = {
+         1: 1          # BYTE An 8-bit unsigned integer.,
+       , 2: 1          # ASCII An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL.,
+       , 3: 2          # SHORT A 16-bit (2-byte) unsigned integer,
+       , 4: 4          # LONG A 32-bit (4-byte) unsigned integer,
+       , 5: 8          # RATIONAL Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator.,
+       , 7: 1          # UNDEFINED An 8-bit byte that can take any value depending on the field definition,
+       , 9: 4          # SLONG A 32-bit (4-byte) signed integer (2's complement notation),
+       , 10 : 8        # SRATIONAL Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator.
+}
+
+# tags for parsing metadata
+Exif_IFD_POINTER = 0x8769
+GPS_INFO_IFD_POINTER = 0x8825
+INTEROPERABILITY_IFD_POINTER = 0xA005
+
+# tags to get thumbnail
+COMPRESSION_SCHEME = 0x103
+COMPRESSION_SCHEME_TYPES = {1:'image/bmp', 6:'image/jpeg'}
+OFFSET_TO_JPEG_SOI = 0x201
+BYTES_OF_JPEG_DATA = 0x202
+STRIPOFFSETS = 0x111
+STRIPBYTECOUNTS = 0x117
+
+# constants for writing
+INTEROPERABILITY_FIELD_LENGTH = 12
+POINTER_TAGS = { Exif_IFD_POINTER:True
+                          , GPS_INFO_IFD_POINTER:True
+                          , INTEROPERABILITY_IFD_POINTER:True}
+
+
+class Exif(dict) :
+       
+       def __init__(self, f) :
+               # File Headers are 8 bytes as defined in the TIFF standard.
+               self.f = f
+               
+               byteOrder = f.read(2)
+               self.byteOrder = byteOrder
+               
+               if byteOrder == 'MM' :
+                       r16 = self.r16 = lambda:ib16(f.read(2))
+                       r32 = self.r32 = lambda:ib32(f.read(4))
+               elif byteOrder == 'II' :
+                       r16 = self.r16 = lambda:il16(f.read(2))
+                       r32 = self.r32 = lambda:il32(f.read(4))
+               else :
+                       raise ValueError, "Unkwnown byte order: %r" % byteOrder
+               
+               assert r16() == 0x002A, "Incorrect exif header"
+               
+               self.tagReaders = {
+                         1:  lambda c : [ord(f.read(1)) for i in xrange(c)]
+                       , 2:  lambda c : f.read(c)
+                       , 3:  lambda c : [r16() for i in xrange(c)]
+                       , 4:  lambda c : [r32() for i in xrange(c)]
+                       , 5:  lambda c : [(r32(), r32()) for i in xrange(c)]
+                       , 7:  lambda c : f.read(c)
+                       , 9:  lambda c : [r32() for i in xrange(c)]
+                       , 10: lambda c : [(r32(), r32()) for i in xrange(c)]
+               }
+               
+               self.tagInfos = {}
+               self.mergedTagInfos = {}
+               self.gpsTagInfos = {}
+               
+               ifd0Offset = r32()
+               
+               ifd1Offset = self._loadTagsInfo(ifd0Offset, 'IFD0')
+               others = [(lambda:self[Exif_IFD_POINTER], 'Exif'),
+                                 (lambda:self.get(GPS_INFO_IFD_POINTER), 'GPS'),
+                                 (lambda:self.get(INTEROPERABILITY_IFD_POINTER), 'Interoperability'),
+                                 (lambda:ifd1Offset, 'IFD1')]
+               
+               self.ifdnames = ['IFD0']
+               
+               for startfunc, ifdname in others :
+                       start = startfunc()
+                       if start :
+                               ret = self._loadTagsInfo(start, ifdname)
+                               assert ret == 0
+                               self.ifdnames.append(ifdname)
+               
+               
+       def _loadTagsInfo(self, start, ifdname) :
+               r16, r32 = self.r16, self.r32
+               
+               self.f.seek(start)
+               
+               numberOfFields = r16()
+               ifdInfos = self.tagInfos[ifdname] = {}
+               
+               for i in xrange(numberOfFields) :
+                       #  12 bytes of the field Interoperability
+                       tag = r16()
+                       typ = r16()
+                       count = r32()
+
+                       ts = TYPES_SIZES[typ]
+                       size = ts * count
+                       
+                       # In cases where the value fits in 4 bytes,
+                       # the value itself is recorded.
+                       # If the value is smaller than 4 bytes, the value is
+                       # stored in the 4-byte area starting from the left.
+                       if size <= 4 :
+                               offsetIsValue = True
+                               offset = self.tagReaders[typ](count)
+                               if count == 1:
+                                       offset = offset[0]
+                               noise = self.f.read(4 - size)
+                       else :
+                               offsetIsValue = False
+                               offset = r32()
+                       
+                       ifdInfos[tag] = (typ, count, offset, offsetIsValue)
+               
+               if ifdname == 'GPS' :
+                       self.gpsTagInfos.update(ifdInfos)
+               else :
+                       self.mergedTagInfos.update(ifdInfos)
+
+               # return nexf ifd offset
+               return r32()
+       
+       def getThumbnail(self) :
+               if hasattr(self, 'ifd1Offset') :
+                       comp = self[COMPRESSION_SCHEME]
+                       if comp == 6 :
+                               # TODO : handle uncompressed thumbnails
+                               mime = COMPRESSION_SCHEME_TYPES.get(comp, 'unknown')
+                               start = self[OFFSET_TO_JPEG_SOI]
+                               count = self[BYTES_OF_JPEG_DATA]
+                               f = self.f
+                               f.seek(start)
+                               data = f.read(count)
+                               return data, mime
+                       else :
+                               return None
+               else :
+                       return None
+       
+               
+       
+       #
+       # dict interface
+       #
+       def keys(self) :
+               return self.mergedTagInfos.keys()
+       
+       def has_key(self, key) :
+               return self.mergedTagInfos.has_key(key)
+       
+       __contains__ = has_key # necessary ?
+       
+       def __getitem__(self, key) :
+               typ, count, offset, offsetIsValue = self.mergedTagInfos[key]
+               if offsetIsValue :
+                       return offset
+               else :
+                       self.f.seek(offset)
+                       value = self.tagReaders[typ](count)
+                       if count == 1:
+                               return value[0]
+                       else :
+                               return value
+       
+       def get(self, key) :
+               if self.has_key(key):
+                       return self[key]
+               else :
+                       return None
+       
+       def getIFDNames(self) :
+               return self.ifdnames
+               
+               
+       def getIFDTags(self, name) :
+               tags = [tag for tag in self.tagInfos[name].keys()]
+               tags.sort()
+               return tags
+               
+
+       def save(self, out) :
+               byteOrder = self.byteOrder
+               
+               if byteOrder == 'MM' :
+                       w16 = self.w16 = lambda i : out.write(ob16(i))
+                       w32 = self.w32 = lambda i : out.write(ob32(i))
+               elif byteOrder == 'II' :
+                       w16 = self.w16 = lambda i : out.write(ol16(i))
+                       w32 = self.w32 = lambda i : out.write(ol32(i))
+               
+               tagWriters = {
+                         1:  lambda l : [out.write(chr(i)) for i in l]
+                       , 2:  lambda l : out.write(l)
+                       , 3:  lambda l : [w16(i) for i in l]
+                       , 4:  lambda l : [w32(i) for i in l]
+                       , 5:  lambda l : [(w32(i[0]), w32(i[1])) for i in l]
+                       , 7:  lambda l : out.write(l)
+                       , 9:  lambda l : [w32(i) for i in l]
+                       , 10: lambda l : [(w32(i[0]), w32(i[1])) for i in l]
+               }
+               
+               
+               # tiff header
+               out.write(self.byteOrder)
+               w16(0x002A)
+               tags = self.keys()
+               r32(8) # offset of IFD0
+               ifdStarts = {}
+               pointerTags = []
+               isPtrTag = POINTER_TAGS.has_key
+               
+               for ifdname in self.getIFDName() :
+                       ifdInfos = self.tagInfos[name]
+                       tags = ifdInfos.keys()
+                       tags.sort()
+                       
+                       ifdStarts[ifdname] = out.tell()
+                       
+                       tiffOffset = ifdStarts[ifdname] + INTEROPERABILITY_FIELD_LENGTH * len(tags) + 4
+                       moreThan4bytesValuesTags = []
+                       
+                       for tag, info in ifdInfos.items() :
+                               if isPtrTag(tag) :
+                                       pointerTags.append((tag, out.tell()))
+                               typ, count, offset, offsetIsValue = info
+
+                               w16(tag)
+                               w16(typ)
+                               w32(count)
+                               
+                               ts = TYPES_SIZES[typ]
+                               size = ts * count
+
+                               if size <= 4 :
+                                       if count == 1 : offset = [offset]                                       
+                                       tagWriters[typ](offset)
+                                       
+                                       # padding
+                                       for i in range(4 - size) : out.write('\0')
+                               else :
+                                       w32(tiffOffset)
+                                       tiffOffset += size
+                                       moreThan4bytesValuesTags.append(tag)
+                       
+                       for tag in moreThan4bytesValuesTags :
+                               typ, count, offset, offsetIsValue = ifdInfos[tag]
+                               self.f.seek(offset)
+                               size = TYPES_SIZES[typ] * count
+                               out.write(self.f.read(size))
+                       
+                       # write place-holder for next ifd offset (updated later)
+                       r32(0)
+                       
+
+def ib16(c):
+       return ord(c[1]) + (ord(c[0])<<8)
+def ob16(i) :
+       return chr(i >> 8 & 255) + chr(i & 255)
+       
+def ib32(c):
+       return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24)
+def ob32(c):
+       return chr(i >> 24 & 0xff) + chr(i >> 16 & 0xff) + chr(i >> 8 & 0xff) + chr(i & 0xff)
+
+
+def il16(c):
+       return ord(c[0]) + (ord(c[1])<<8)
+def ol16(i):
+       return chr(i&255) + chr(i>>8&255)
+       
+def il32(c):
+       return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24)
+def ol32(i):
+       return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255)
+
+
+
+
+def testRead(*paths) :
+       from PIL.Image import open as imgopen
+       from standards.exif import TAGS
+       from cStringIO import StringIO
+       
+       import os
+       paths = list(paths)
+       paths.extend(['testimages/%s'%name for name in os.listdir('testimages') \
+                                       if name.endswith('.jpg') and \
+                                          not name.endswith('_thumb.jpg')])
+       
+       for path in paths :
+               print '------------'
+               print path
+               print '------------'
+               im = imgopen(path)
+               applist = im.applist
+               exifBlock = [a[1] for a in applist if a[0] == 'APP1' and a[1].startswith("Exif\x00\x00")][0]
+               exif = exifBlock[6:]
+               sio = StringIO(exif)
+
+               e = Exif(sio)
+               for name in e.getIFDNames() :
+                       print '%s: ' %name
+                       for tag in e.getIFDTags(name) :
+                               print hex(tag), TAGS.get(tag), e[tag]
+                       print
+               
+               thumb = e.getThumbnail()
+               if thumb is not None :
+                       data, mime = thumb
+                       out = open('%s_thumb.jpg' % path[:-4], 'w')
+                       out.write(data)
+                       out.close()
+
+def testWrite(*paths) :
+       from PIL.Image import open as imgopen
+       from standards.exif import TAGS
+       from cStringIO import StringIO
+       
+#      import os
+#      paths = list(paths)
+#      paths.extend(['testimages/%s'%name for name in os.listdir('testimages') \
+#                                      if name.endswith('.jpg') and \
+#                                         not name.endswith('_thumb.jpg')])
+       
+       for path in paths :
+               print '------------'
+               print path
+               print '------------'
+               im = imgopen(path)
+               applist = im.applist
+               exifBlock = [a[1] for a in applist if a[0] == 'APP1' and a[1].startswith("Exif\x00\x00")][0]
+               exif = exifBlock[6:]
+               from cStringIO import StringIO
+               sio = StringIO(exif)
+
+               e = Exif(sio)
+               
+               out = StringIO()
+               e.save(out)
+               out.seek(0)
+               print '%r' % out.read()
+               
+
+if __name__ == '__main__' :
+       testRead('testMM.jpg', 'testII.jpg')
+       #testWrite('testMM.jpg', 'testII.jpg')