root/trunk/tools.py
| Revision 201 (by manatlan, 04/07/08 12:48:37) |
|---|
# -*- coding: utf-8 -*- ## ## Copyright (C) 2005 manatlan manatlan[at]gmail(dot)com ## ## 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; version 2 only. ## ## 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. ## # here is the worst code of jbrout (but it works well ;-) # this file should be redone from scratch, one day ... manatlan import time import sys,os,string,re from datetime import timedelta import datetime from subprocess import Popen,PIPE import tempfile # Protect against crappy output of some unnamed (ehm, # ehm, jhead) programs. Prepare tables for later use. nonPCData = '' # additional procedure for working with Unicode -- normal # string.maketrans doesn't work with Unicode strings. # originally from # http://groups.google.com/group/comp.lang.python/msg/4dbebae9e040a7b3 def maketransU(s1, s2, todel=""): trans_tab = dict( zip( map(ord, s1), map(ord, s2) ) ) trans_tab.update( (ord(c),None) for c in todel ) return trans_tab # These are codes of characters which are not PCDATA # and so they cannot happen in XML file. nonPCDataRange = range(0x00,0x08)+[0x0b,0x0c]+range(0x0e,0x19) # The following ones are stricly speaking not incorrect (and XML # parse won't choke on them), but they are unprintable control # characters, so they will probably never happen in metadata. nonPCDataRange += range(0x7f,0x9f) for i in nonPCDataRange: nonPCData += chr(i) allchars = maketransU('','',nonPCData) def cd2d(f): #yyyymmddhhiiss -> datetime return datetime.datetime(int(f[:4]),int(f[4:6]), int(f[6:8]),int(f[8:10]),int(f[10:12]),int(f[12:14])) def ed2cd(f): #yyyy/mm/dd hh:ii:ss -> yyyymmddhhiiss if f: return f[:4]+f[5:7]+f[8:10]+f[11:13]+f[14:16]+f[17:19] else: return f def splitFile(ffile): ffile = ffile.replace("\\","/") if ffile[-1:]=="/": ffile = ffile[:-1] sdir = os.path.dirname(ffile)+"/" sfile = os.path.basename(ffile) return sdir,sfile class CommandException(Exception): def __init__(self,m): self.message=m def __str__(self): return self.message # ############################################################################################## class _Command: # ############################################################################################## """ low-level access (wrapper) to external tools used in jbrout """ format = "p%Y%m%d_%H%M%S" #format = "%Y-%m-%d_%H-%M-%S" isWin=(sys.platform[:3] == "win") __path =os.path.join(os.getcwdu(),u"tools") err="" if isWin: # set windows path __jhead = os.path.join(__path,"jhead.exe") __exiftran = None __jpegtran = os.path.join(__path,"jpegtran.exe") __jpegnail = os.path.join(__path,"jpegnail.exe") __exifedit = os.path.join(__path,"exifedit.exe") #perhaps it's there ! if not os.path.isfile(__jhead): err+="jhead is not present in 'tools'\n" if not os.path.isfile(__jpegtran): err+="jpegtran is not present in 'tools'\n" if not os.path.isfile(__jpegnail): err+="jpegnail is not present in 'tools'\n" else: # set "non windows" path (needs 'which') __jhead = u"".join(os.popen("which jhead").readlines()).strip() __exiftran = u"".join(os.popen("which exiftran").readlines()).strip() __jpegtran = None __jpegnail = None __exifedit = None if not os.path.isfile(__jhead): err+="jhead is not present, please install 'jhead'\n" if not os.path.isfile(__exiftran): err+="exiftran is not present, please install 'exiftran'(fbida)\n" if err: raise CommandException(err) @staticmethod def _run(cmds): cmdline = str( [" ".join(cmds)] ) # to output easily (with strange chars) try: cmds = [i.encode(sys.getfilesystemencoding()) for i in cmds] except: raise CommandException( cmdline +"\n encoding trouble") p = Popen(cmds, shell=False,stdout=PIPE,stderr=PIPE) time.sleep(0.01) # to avoid "IOError: [Errno 4] Interrupted system call" out = string.join(p.stdout.readlines() ).strip() outerr = string.join(p.stderr.readlines() ).strip() if "jhead" in cmdline: if "Nonfatal Error" in outerr: # possible "Suspicious offset of first IFD value" (non fatal error of jhead) outerr="" if "exiftran" in cmdline: if "processing" in outerr: # exiftran output process in stderr ;-( outerr="" if outerr: raise CommandException( cmdline +"\n OUTPUT ERROR:"+outerr) else: try: out = out.decode("utf_8") # recupere les infos en UTF_8 except: try: out = out.decode("latin_1") # recupere les anciens infos (en latin_1) except UnicodeDecodeError: try: out = out.decode(sys.getfilesystemencoding()) except UnicodeDecodeError: raise CommandException( cmdline +"\n decoding trouble") # Protect against crappy output of some unnamed (ehm, # ehm, jhead) programs. Use tables from the top of this # module. out = out.translate(allchars) return out #unicode #---------------------------------------------------------------------------- @staticmethod def setDate(file, newDate ): #---------------------------------------------------------------------------- """ set the Exif and File dates of file 'file' to date 'newDate' """ _Command._run( [_Command.__jhead,"-ts"+newDate.strftime("%Y:%m:%d-%H:%M:%S"),'-ft',file] ) #---------------------------------------------------------------------------- @staticmethod def getExifInfo(file): #---------------------------------------------------------------------------- """ get the result Exif of jhead """ return _Command._run( [_Command.__jhead,file] ) #---------------------------------------------------------------------------- @staticmethod def prepareFile(file,needRename=True,needAutoRot=False): #---------------------------------------------------------------------------- """ prepare the file (rename/rotate according exif) ... (in one jhead action (optimization)) if needRename : rename file according its exifdate, and set datefile = date exif, and return the new name (or old one if not renamed) and do autorot according exif tag if "needAutoRot" is True """ if _Command.isWin: needAutoRot = False # no autorotate on windows (jpegtran trouble) if needRename: # renaming is needed, we'll need to return the new name if needAutoRot: buf = _Command._run( [_Command.__jhead,"-ft","-autorot","-nf"+_Command.format,file] ) else: buf = _Command._run( [_Command.__jhead,'-ft',"-nf"+_Command.format,file] ) # rename has be done, we need to get the newname # (we get it in jhead output) p = buf.find("-->") if p>0: # it was renamed return buf[p+3:].strip() else: # not renamed, return original name return file else: # no renaming, return the original filename (to be compatible) if needAutoRot: _Command._run( [_Command.__jhead,"-ft","-autorot",file] ) return file #---------------------------------------------------------------------------- @staticmethod def getExif(file): #---------------------------------------------------------------------------- """ return a dict of exif info of the file(jpeg) 'file' filedate,resolution,exifdate,isflash,jpegcomment """ tag = {} buf = _Command._run( [_Command.__jhead,file] ) assert type(buf)==unicode try: tag["filedate"] = re.findall( "File date : (\d\d\d\d:\d\d:\d\d \d\d:\d\d:\d\d)", buf )[0].strip() tag["resolution"] = re.findall( "Resolution : (.*)", buf )[0].strip() except: raise CommandException( "Exif decoding trouble in "+file +"\n"+buf) # try to get exif info (from jhead) try: exifdate = re.findall( "Date/Time : (\d\d\d\d:\d\d:\d\d \d\d:\d\d:\d\d)", buf )[0].strip() isflash = re.findall( "Flash used : (.*)", buf )[0].strip() except IndexError: exifdate ="" isflash ="" try: cd2d(ed2cd(exifdate)) # just to test if it's a real date except: exifdate = "" tag["exifdate"] =exifdate tag["isflash"] =isflash # get the comment (which can be multilines) mo_comm = re.findall( "Comment : (.*)", buf ) comment = u"" for i in mo_comm: if comment != "" : comment = comment + "\n" comment = comment + i.strip() tag["jpegcomment"]=comment # convert date to format yyyymmddhhiiss tag["filedate"]=ed2cd(tag["filedate"]) tag["exifdate"]=ed2cd(tag["exifdate"]) return tag #---------------------------------------------------------------------------- @staticmethod def setJpegComment(file,buf): #---------------------------------------------------------------------------- """ set the jpegcomment 'buf' in the picture 'file' """ if buf: assert type(buf)==unicode tmp_fd, tmp_name = tempfile.mkstemp(prefix='jbrout') tf = os.fdopen(tmp_fd, 'w') if tf: tf.write( buf.encode("utf_8") ) # comment in UTF8 *new* tf.close() _Command._run( [_Command.__jhead,'-ci',tmp_name,file] ) try: os.unlink(tmp_name) except: pass return True return False else: # kill the jpeg comment _Command._run( [_Command.__jhead,'-dc',file] ) return True #---------------------------------------------------------------------------- @staticmethod def rotate(file,sens): #---------------------------------------------------------------------------- """ rotate the picture 'file', and its internal thumbnail according 'sens' (R/L)""" if sens=="R": deg = "90" opt = "-9" else: deg = "270" opt = "-2" if _Command.isWin: b= _Command._run( [_Command.__jpegtran,'-rotate',deg,'-copy','all',file,file] ) # rebuild the exif thumb, because jpegtran doesn't do _Command.rebuildExifThumb(file) else: b= _Command._run( [_Command.__exiftran,opt,'-ip',file] ) return b #---------------------------------------------------------------------------- @staticmethod def rebuildExifThumb(file): #---------------------------------------------------------------------------- """ rebuild the internal thumbnail of the picture 'file' """ if _Command.isWin: if os.path.isfile( _Command.__exifedit ): # if exifedit is here # it's a lot better to use exifedit on win32 _Command._run( [_Command.__exifedit,"/t","a,160","/b",file] ) else: _Command._run( [_Command.__jpegnail, '-q', '75', '-x', '160', '-y', '160',file ] ) else: _Command._run( [_Command.__exiftran, '-g', '-ip', file ] ) #---------------------------------------------------------------------------- @staticmethod def removeExif(file): #---------------------------------------------------------------------------- """ remove all exif tags of picture 'file' """ _Command._run( [_Command.__jhead,'-de',"-dt","-dc",file] ) #---------------------------------------------------------------------------- @staticmethod def copyExif(file1,file2): #---------------------------------------------------------------------------- """ copy exif info from file1 to file2 (and redate filedate of file2) """ _Command._run( [_Command.__jhead,"-ft",'-te',file1,file2] ) class DateSave: def __init__(self,file): """ save dates info """ self.file = file s = os.stat(file) self.atime = s.st_atime self.mtime = s.st_mtime def touch(self): """ return the error or "" if not """ try: os.utime(self.file,(self.atime,self.mtime)) return "" except OSError,detail: # utime doesn't work well if uid/gid are not the same # so we need to use the real touch ;-) (with mtime) print "need to touch ;-(" stime = time.strftime("%Y%m%d%H%M.%S",time.localtime(self.mtime) ) _Command._run( ["touch",'-t',stime,self.file] ) return "" def redate(self,w,d,h,m,s ): t= time.localtime(self.mtime) dt = datetime.datetime(t[0],t[1],t[2],t[3],t[4],t[5]) dt+=datetime.timedelta(d,s,0,0,m,h,w) t = dt.timetuple() self.mtime= time.mktime(t) self.atime= self.mtime self.touch() # ============================================================================================ class OldPhotoCmd: # ============================================================================================ """ Manipulate photos(jpg) """ @staticmethod def normalizeName(file): """ normalize name (only real exif pictures !!!!) """ assert type(file)==unicode return _Command.normalizeName(file) def __init__(self,file): assert os.path.isfile(file) assert type(file)==unicode,"ERROR:"+str(type(file)) self.file = file self.__read() # save filesystem mtime/atime self.ds = DateSave(self.file) def __read(self): assert os.path.isfile(self.file) t = _Command.getExif(self.file) self.comment = re.sub( u"%.*%", u"", t["jpegcomment"] ) self.tags = self._extractTags( t["jpegcomment"] ) self.filedate = t["filedate"] self.resolution = t["resolution"] self.exifdate = t["exifdate"] self.isflash = t["isflash"] self.tags.sort() def _extractTags(self,buf): mo = re.findall( "%.*%", buf ) if mo: tags = mo[0] self.start = string.find(buf,tags) self.end = self.start + len(tags) t = tags.split("%") del(t[0]) del(t[-1]) else: t = [] return t def sub(self,t): if t in self.tags: self.tags.remove(t) return self._write() else: return False def add(self,t): if t in self.tags: return False else: self.tags.append(t) return self._write() def addTags(self,tags): # *new* """ add a list of tags to the file, return False if it can't """ isModified = False for t in tags: if t not in self.tags: isModified = True self.tags.append(t) if isModified: return self._write() return True def subTags(self,tags): # *new* """ sub a list of tags to the file, return False if it can't """ isModified = False for t in tags: if t in self.tags: isModified = True self.tags.remove(t) if isModified: return self._write() return True def clear(self): self.tags = [] return self._write() def addComment(self,c): self.comment = c return self._write() def _write(self): """ writes tags/comment (sub(),add(),addTags(),subTags(),clear(),addComment()) return True if tags/comment were written, false if not """ if len(self.tags)>0: self.tags.sort() msg = "%"+ ("%".join(self.tags)) +"%" else: msg = "" buf = self.comment + msg if _Command.setJpegComment(self.file,buf): self.ds.touch() # retouch filesystem mtime/atime return True else: return False def redate(self,w,d,h,m,s ): """ redate jpeg file from offset : weeks, days, hours, minutes,seconds if exif : redate internal date and fs dates (jhead -ft) if not : redate file dates (atime/mtime) """ if self.exifdate: # redate internal exif date # and redate "filesystem dates" with "jhead -ft" !!! newDate=cd2d(self.exifdate) newDate+=timedelta(weeks=w, days=d,hours=h,minutes=m,seconds=s) _Command.setDate(self.file,newDate ) else: # ONLY redate "filesystem dates" self.ds.redate(w,d,h,m,s ) self.__read() # read again, exifdate has changed def rotate(self,sens): _Command.rotate(self.file,sens) self.ds.touch() # retouch filesystem mtime/atime self.__read() # read again, because resolution has changed def rebuildExifTB(self): _Command.rebuildExifThumb(self.file) self.ds.touch() # retouch filesystem mtime/atime self.__read() # read again, perhaps something has changed def repair(self): _Command.removeExif(self.file) self.ds.touch() # retouch filesystem mtime/atime self.__read() # read again, perhaps something has changed from libs.iptcinfo import IPTCInfo # ============================================================================================ class PhotoCmd(object): # ============================================================================================ """ Manipulate photos(jpg) """ @staticmethod def normalizeName(file): """ normalize name (only real exif pictures !!!!) """ assert type(file)==unicode return _Command.prepareFile(file,needRename=True,needAutoRot=False) @staticmethod def prepareFile(file,needRename,needAutoRot): """ prepare file, rotating/autorotating according exif tags (same things as normalizename + autorot, in one action) only called at IMPORT/REFRESH albums """ assert type(file)==unicode return _Command.prepareFile(file,needRename,needAutoRot) @staticmethod def setNormalizeNameFormat(format): """ set format for normalized files (see prepareFile())""" _Command.format = format def __init__(self,file): assert type(file)==unicode,"ERROR:"+str(type(file)) assert os.path.isfile(file) self.file = file self.__read() # save filesystem mtime/atime self.ds = DateSave(self.file) def __getTags(self): return self.__iptc.keywords tags = property(__getTags) def __read(self): t = _Command.getExif(self.file) self.comment = t["jpegcomment"] self.filedate = t["filedate"] self.resolution = t["resolution"] self.exifdate = t["exifdate"] self.isflash = t["isflash"] self.readonly = not os.access( self.file, os.W_OK) self.__iptc = IPTCInfo(self.file,True) self.__iptc.keywords.sort() def sub(self,t): assert type(t)==unicode if t in self.__iptc.keywords: self.__iptc.keywords.remove(t) return self._write() else: return False def add(self,t): assert type(t)==unicode if t in self.__iptc.keywords: return False else: self.__iptc.keywords.append(t) return self._write() def addTags(self,tags): # *new* """ add a list of tags to the file, return False if it can't """ isModified = False for t in tags: assert type(t)==unicode if t not in self.__iptc.keywords: isModified = True self.__iptc.keywords.append(t) if isModified: return self._write() return True def _setCommentAndTags(self,c,t): # **special convert** assert type(c)==unicode,str(type(c))+" : "+c for i in t: assert type(i)==unicode pass self.comment = c self.__iptc.keywords = t return self._write() def subTags(self,tags): # *new* """ sub a list of tags to the file, return False if it can't """ isModified = False for t in tags: assert type(t)==unicode if t in self.__iptc.keywords: isModified = True self.__iptc.keywords.remove(t) if isModified: return self._write() return True def clear(self): self.__iptc.keywords = [] return self._write() def addComment(self,c): assert type(c)==unicode self.comment = c return self._write() def _write(self): """ writes tags/comment (sub(),add(),addTags(),subTags(),clear(),addComment()) return True if tags/comment were written, false if not """ if self.__iptc.save(): _Command.setJpegComment(self.file,self.comment) self.ds.touch() # retouch filesystem mtime/atime return True else: return False def redate(self,w,d,h,m,s ): """ redate jpeg file from offset : weeks, days, hours, minutes,seconds if exif : redate internal date and fs dates (jhead -ft) if not : redate file dates (atime/mtime) """ if self.exifdate: # redate internal exif date # and redate "filesystem dates" with "jhead -ft" !!! newDate=cd2d(self.exifdate) newDate+=timedelta(weeks=w, days=d,hours=h,minutes=m,seconds=s) _Command.setDate(self.file,newDate ) else: # ONLY redate "filesystem dates" self.ds.redate(w,d,h,m,s ) self.__read() # read again, exifdate has changed def rotate(self,sens): _Command.rotate(self.file,sens) self.ds.touch() # retouch filesystem mtime/atime self.__read() # read again, because resolution has changed def rebuildExifTB(self): _Command.rebuildExifThumb(self.file) self.ds.touch() # retouch filesystem mtime/atime self.__read() # read again, perhaps something has changed def getExifInfo(self): """ return the result of jhead on this file """ return _Command.getExifInfo(self.file) def destroyInfo(self): """ destroy info (exif/iptc) of the file but keep filesystem date *IMPORTANT* : it doesn't kill IPTC-block, only KEYWORDS ! """ _Command.removeExif(self.file) self.clear() # clear IPTC tags self.ds.touch() # retouch filesystem mtime/atime self.__read() # read again, perhaps something has changed def copyInfoTo(self,file2): """ copy exif/iptc to "file2" and redate filesystem date of file2 according exif """ assert type(file2)==unicode #copy iptc tags i=IPTCInfo(file2,True) i.keywords=[] i.keywords+=self.__iptc.keywords i.save() # copy exif (exif, thumb + jpegcomment), and redate _Command.copyExif(self.file,file2) # and rebuild i-thumb (should set good resolution in exif) ds = DateSave(file2) _Command.rebuildExifThumb(file2) ds.touch() if __name__ == "__main__": f="/home/manatlan/Desktop/test_jbrout/100_fuji/p20041126_230000.jpg" #~ f=u"phôto.jpg" #~ f=u"photo.jpg" #~ f="/home/manatlan/Desktop/100_FUJI_recentes/p20060714_205539.jpg" #~ print _Command.getExifInfo(f) def test_Command(f): c=_Command com = u"""héllçàé€$£'"€&&<>~^à""" t=c.getExif(f) res = t["resolution"] c.setJpegComment(f,com) c.rotate(f,"L") t=c.getExif(f) com2 = t["jpegcomment"] print [com,com2] assert res != t["resolution"], "rotate is bad" #~ assert com == com2, "jpegcomment problem" c.rotate(f,"R") c.setJpegComment(f,"") newf="jp.jpg" import shutil shutil.copy(f,newf) d=datetime.datetime(2000,11,14,12,10,59) c.setDate(newf,d) t=c.getExif(newf) assert cd2d(t["filedate"]) == cd2d(t["exifdate"]) == d,"dates problem" newNewf = c.normalizeName(newf) c.rebuildExifThumb(newNewf) c.removeExif(newNewf) os.unlink(newNewf) #raise error if a problem #~ test_Command(f) buf=""" Camera make : FUJIFILM Camera model : FinePix F11 Date/Time : 2008:03:28 21:23:41 Resolution : 2048 x 1536 Flash used : Yes (auto) """ print re.findall( "Date/Time : (\d\d\d\d:\d\d:\d\d \d\d:\d\d:\d\d)", buf )[0].strip()
Note: See TracBrowser for help on using the browser.
