# -*- 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')
