--- /dev/null
+# -*- 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')