X-Git-Url: https://scm.cri.minesparis.psl.eu/git/Photo.git/blobdiff_plain/b0a7e10b4f32cf74864bb53268ca4d3080f23bc0..6c41809185e322ce2d30e98234f71144f78f06c0:/Products/Photo/exif.py?ds=sidebyside diff --git a/Products/Photo/exif.py b/Products/Photo/exif.py new file mode 100755 index 0000000..8120852 --- /dev/null +++ b/Products/Photo/exif.py @@ -0,0 +1,374 @@ +# -*- coding: utf-8 -*- +####################################################################################### +# Photo is a part of Plinn - http://plinn.org # +# Copyright © 2008 Benoît PIN # +# # +# 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')