+++ /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')