Package imagizer :: Module imagizer
[hide private]
[frames] | no frames]

Source Code for Module imagizer.imagizer

   1  #!/usr/bin/env python  
   2  # -*- coding: UTF8 -*- 
   3  #******************************************************************************\ 
   4  #* $Source$ 
   5  #* $Id$ 
   6  #* 
   7  #* Copyright (C) 2006 - 2010,  Jérôme Kieffer <kieffer@terre-adelie.org> 
   8  #* Conception : Jérôme KIEFFER, Mickael Profeta & Isabelle Letard 
   9  #* Licence GPL v2 
  10  #* 
  11  #* This program is free software; you can redistribute it and/or modify 
  12  #* it under the terms of the GNU General Public License as published by 
  13  #* the Free Software Foundation; either version 2 of the License, or 
  14  #* (at your option) any later version. 
  15  #* 
  16  #* This program is distributed in the hope that it will be useful, 
  17  #* but WITHOUT ANY WARRANTY; without even the implied warranty of 
  18  #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  19  #* GNU General Public License for more details. 
  20  #* 
  21  #* You should have received a copy of the GNU General Public License 
  22  #* along with this program; if not, write to the Free Software 
  23  #* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
  24  #* 
  25  #*****************************************************************************/ 
  26   
  27  """ 
  28  General library used by selector and generator. 
  29  It handles images, progress bars and configuration file. 
  30  """ 
  31   
  32  import os, sys, shutil, time, re, gc, distutils.sysconfig 
  33   
  34  try: 
  35      import Image, ImageStat, ImageChops, ImageFile 
  36  except: 
  37      raise ImportError("Selector needs PIL: Python Imaging Library\n PIL is available from http://www.pythonware.com/products/pil/") 
  38  try: 
  39      import pygtk ; pygtk.require('2.0') 
  40      import gtk 
  41      import gtk.glade as GTKglade 
  42  except ImportError: 
  43      raise ImportError("Selector needs pygtk and glade-2 available from http://www.pygtk.org/") 
  44  #Variables globales qui sont des CONSTANTES ! 
  45  gtkInterpolation = [gtk.gdk.INTERP_NEAREST, gtk.gdk.INTERP_TILES, gtk.gdk.INTERP_BILINEAR, gtk.gdk.INTERP_HYPER] 
  46  #gtk.gdk.INTERP_NEAREST    Nearest neighbor sampling; this is the fastest and lowest quality mode. Quality is normally unacceptable when scaling down, but may be OK when scaling up. 
  47  #gtk.gdk.INTERP_TILES    This is an accurate simulation of the PostScript image operator without any interpolation enabled. Each pixel is rendered as a tiny parallelogram of solid color, the edges of which are implemented with antialiasing. It resembles nearest neighbor for enlargement, and bilinear for reduction. 
  48  #gtk.gdk.INTERP_BILINEAR    Best quality/speed balance; use this mode by default. Bilinear interpolation. For enlargement, it is equivalent to point-sampling the ideal bilinear-interpolated image. For reduction, it is equivalent to laying down small tiles and integrating over the coverage area. 
  49  #gtk.gdk.INTERP_HYPER    This is the slowest and highest quality reconstruction function. It is derived from the hyperbolic filters in Wolberg's "Digital Image Warping", and is formally defined as the hyperbolic-filter sampling the ideal hyperbolic-filter interpolated image (the filter is designed to be idempotent for 1:1 pixel mapping). 
  50   
  51   
  52  #here we detect the OS runnng the program so that we can call exftran in the right way 
  53  installdir = os.path.join(distutils.sysconfig.get_python_lib(), "imagizer") 
  54  if os.name == 'nt': #sys.platform == 'win32': 
  55      exiftran = os.path.join(installdir, "exiftran.exe ") 
  56      gimpexe = "gimp-remote " 
  57      ConfFile = [os.path.join(os.getenv("ALLUSERSPROFILE"), "imagizer.conf"), os.path.join(os.getenv("USERPROFILE"), "imagizer.conf"), "imagizer.conf"] 
  58  elif os.name == 'posix': 
  59      MaxJPEGMem = 100000 # OK up to 10 Mpix 
  60      exiftran = "JPEGMEM=%i %s " % (MaxJPEGMem, os.path.join(installdir, "exiftran ")) 
  61      gimpexe = "gimp-remote " 
  62      ConfFile = ["/etc/imagizer.conf", os.path.join(os.getenv("HOME"), ".imagizer"), ".imagizer"] 
  63  else: 
  64      raise OSError("Your platform does not seem to be an Unix nor a M$ Windows.\nI am sorry but the exiftran binary is necessary to run selector, and exiftran is probably not available for you plateform. If you have exiftran installed, please contact the developper to correct that bug, kieffer at terre-adelie dot org") 
  65   
  66  #sys.path.append(installdir)     
  67  unifiedglade = os.path.join(installdir, "selector.glade") 
  68  from signals import Signal 
  69  from config import Config 
  70  config = Config() 
  71  config.load(ConfFile) 
  72  if config.ImageCache > 1000: 
  73      import imagecache 
  74      imageCache = imagecache.ImageCache(maxSize=config.ImageCache) 
  75  else: 
  76      imageCache = None 
  77  import pyexiv2 
  78   
  79   
  80   
  81  #class Model: 
  82  #    """ Implémentation de l'applicatif 
  83  #    """ 
  84  #    def __init__(self, label): 
  85  #        """ 
  86  #        """ 
  87  #        self.__label = label 
  88  #        self.startSignal = Signal() 
  89  #        self.refreshSignal = Signal() 
  90  #         
  91  #    def start(self): 
  92  #        """ Lance les calculs 
  93  #        """ 
  94  #        self.startSignal.emit(self.__label, NBVALUES) 
  95  #        for i in xrange(NBVALUES): 
  96  #            time.sleep(0.5) 
  97  #             
  98  #            # On lève le signal de rafraichissement des vues éventuelles 
  99  #            # Note qu'ici on ne sait absolument pas si quelque chose s'affiche ou non 
 100  #            # ni de quelle façon c'est affiché. 
 101  #            self.refreshSignal.emit(i) 
 102   
 103   
 104   
105 -class ModelProcessSelected:
106 """Implemantation MVC de la procedure ProcessSelected"""
107 - def __init__(self):
108 """ 109 """ 110 self.__label = "Un moment..." 111 self.startSignal = Signal() 112 self.refreshSignal = Signal() 113 self.finishSignal = Signal() 114 self.NbrJobsSignal = Signal()
115 - def start(self, List):
116 """ Lance les calculs 117 """ 118 119 def SplitIntoPages(pathday, GlobalCount): 120 """Split a directory (pathday) into pages of 20 images""" 121 files = [] 122 for i in os.listdir(pathday): 123 if os.path.splitext(i)[1] in config.Extensions:files.append(i) 124 files.sort() 125 if len(files) > config.NbrPerPage: 126 pages = 1 + (len(files) - 1) / config.NbrPerPage 127 for i in range(1, pages + 1): 128 folder = os.path.join(pathday, config.PagePrefix + str(i)) 129 if not os.path.isdir(folder): mkdir(folder) 130 for j in range(len(files)): 131 i = 1 + (j) / config.NbrPerPage 132 filename = os.path.join(pathday, config.PagePrefix + str(i), files[j]) 133 self.refreshSignal.emit(GlobalCount, files[j]) 134 GlobalCount += 1 135 shutil.move(os.path.join(pathday, files[j]), filename) 136 ScaleImage(filename, filigrane) 137 else: 138 for j in files: 139 self.refreshSignal.emit(GlobalCount, j) 140 GlobalCount += 1 141 ScaleImage(os.path.join(pathday, j), filigrane) 142 return GlobalCount
143 def ArrangeOneFile(dirname, filename): 144 try: 145 timetuple = time.strptime(filename[:19], "%Y-%m-%d_%Hh%Mm%S") 146 suffix = filename[19:] 147 except ValueError: 148 try: 149 timetuple = time.strptime(filename[:11], "%Y-%m-%d_") 150 suffix = filename[11:] 151 except ValueError: 152 print("Unable to handle such file: %s" % filename) 153 return 154 daydir = os.path.join(SelectedDir, time.strftime("%Y-%m-%d", timetuple)) 155 if not os.path.isdir(daydir): 156 os.mkdir(daydir) 157 shutil.move(os.path.join(dirname, filename), os.path.join(daydir, time.strftime("%Hh%Mm%S", timetuple) + suffix))
158 159 self.startSignal.emit(self.__label, max(1, len(List))) 160 if config.Filigrane: 161 filigrane = signature(config.FiligraneSource) 162 else: 163 filigrane = None 164 165 SelectedDir = os.path.join(config.DefaultRepository, config.SelectedDirectory) 166 self.refreshSignal.emit(-1, "copie des fichiers existants") 167 if not os.path.isdir(SelectedDir): mkdir(SelectedDir) 168 #####first of all : copy the subfolders into the day folder to help mixing the files 169 AlsoProcess = 0 170 for day in os.listdir(SelectedDir): 171 #if SingleDir : revert to a foldered structure 172 DayOrFile = os.path.join(SelectedDir, day) 173 if os.path.isfile(DayOrFile): 174 ArrangeOneFile(SelectedDir, day) 175 AlsoProcess += 1 176 #end SingleDir normalization 177 elif os.path.isdir(DayOrFile): 178 if day in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]: 179 recursive_delete(DayOrFile) 180 elif day.find(config.PagePrefix) == 0: #subpages in SIngleDir mode that need to be flatten 181 for File in os.listdir(DayOrFile): 182 if os.path.isfile(os.path.join(DayOrFile, File)): 183 ArrangeOneFile(DayOrFile, File) 184 AlsoProcess += 1 185 # elif os.path.isdir(os.path.join(DayOrFile,File)) and File in [config.ScaledImages["Suffix"],config.Thumbnails["Suffix"]]: 186 # recursive_delete(os.path.join(DayOrFile,File)) 187 recursive_delete(DayOrFile) 188 else: 189 for File in os.listdir(DayOrFile): 190 if File.find(config.PagePrefix) == 0: 191 if os.path.isdir(os.path.join(SelectedDir, day, File)): 192 for strImageFile in os.listdir(os.path.join(SelectedDir, day, File)): 193 src = os.path.join(SelectedDir, day, File, strImageFile) 194 dst = os.path.join(SelectedDir, day, strImageFile) 195 if os.path.isfile(src) and not os.path.exists(dst): 196 shutil.move(src, dst) 197 AlsoProcess += 1 198 if (os.path.isdir(src)) and (os.path.split(src)[1] in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]): 199 shutil.rmtree(src) 200 else: 201 if os.path.splitext(File)[1] in config.Extensions: 202 AlsoProcess += 1 203 204 #######then copy the selected files to their folders########################### 205 for File in List: 206 dest = os.path.join(SelectedDir, File) 207 src = os.path.join(config.DefaultRepository, File) 208 destdir = os.path.dirname(dest) 209 if not os.path.isdir(destdir): makedir(destdir) 210 if not os.path.exists(dest): 211 print "copie de %s " % (File) 212 shutil.copy(src, dest) 213 os.chmod(dest, config.DefaultFileMode) 214 AlsoProcess += 1 215 else : 216 print "%s existe déja" % (dest) 217 if AlsoProcess > 0:self.NbrJobsSignal.emit(AlsoProcess) 218 ######copy the comments of the directory to the Selected directory 219 AlreadyDone = [] 220 for File in List: 221 directory = os.path.split(File)[0] 222 if directory in AlreadyDone: 223 continue 224 else: 225 AlreadyDone.append(directory) 226 dst = os.path.join(SelectedDir, directory, config.CommentFile) 227 src = os.path.join(config.DefaultRepository, directory, config.CommentFile) 228 if os.path.isfile(src): 229 shutil.copy(src, dst) 230 231 ########finaly recreate the structure with pages or make a single page ######################## 232 dirs = os.listdir(SelectedDir) 233 dirs.sort() 234 # print "config.ExportSingleDir = "+str(config.ExportSingleDir) 235 if config.ExportSingleDir: #SingleDir 236 #first move all files to the root 237 for day in dirs: 238 daydir = os.path.join(SelectedDir, day) 239 for filename in os.listdir(daydir): 240 try: 241 timetuple = time.strptime(day[:10] + "_" + filename[:8], "%Y-%m-%d_%Hh%Mm%S") 242 suffix = filename[8:] 243 except ValueError: 244 try: 245 timetuple = time.strptime(day[:10], "%Y-%m-%d") 246 suffix = filename 247 except ValueError: 248 print ("Unable to handle dir: %s\t file: %s" % (day, filename)) 249 continue 250 src = os.path.join(daydir, filename) 251 dst = os.path.join(SelectedDir, time.strftime("%Y-%m-%d_%Hh%Mm%S", timetuple) + suffix) 252 shutil.move(src, dst) 253 recursive_delete(daydir) 254 SplitIntoPages(SelectedDir, 0) 255 else: #Multidir 256 GlobalCount = 0 257 for day in dirs: 258 GlobalCount = SplitIntoPages(os.path.join(SelectedDir, day), GlobalCount) 259 260 self.finishSignal.emit() 261 262 263
264 -class ModelCopySelected:
265 """Implemantation MVC de la procedure CopySelected"""
266 - def __init__(self):
267 """ 268 """ 269 self.__label = "Un moment..." 270 self.startSignal = Signal() 271 self.refreshSignal = Signal() 272 self.finishSignal = Signal() 273 self.NbrJobsSignal = Signal()
274 - def start(self, List):
275 """ Lance les calculs 276 """ 277 self.startSignal.emit(self.__label, max(1, len(List))) 278 if config.Filigrane: 279 filigrane = signature(config.FiligraneSource) 280 else: 281 filigrane = None 282 283 SelectedDir = os.path.join(config.DefaultRepository, config.SelectedDirectory) 284 self.refreshSignal.emit(-1, "copie des fichiers existants") 285 if not os.path.isdir(SelectedDir): mkdir(SelectedDir) 286 #####first of all : copy the subfolders into the day folder to help mixing the files 287 for day in os.listdir(SelectedDir): 288 for File in os.listdir(os.path.join(SelectedDir, day)): 289 if File.find(config.PagePrefix) == 0: 290 if os.path.isdir(os.path.join(SelectedDir, day, File)): 291 for strImageFile in os.listdir(os.path.join(SelectedDir, day, File)): 292 src = os.path.join(SelectedDir, day, File, strImageFile) 293 dst = os.path.join(SelectedDir, day, strImageFile) 294 if os.path.isfile(src) and not os.path.exists(dst): 295 shutil.move(src, dst) 296 if (os.path.isdir(src)) and (os.path.split(src)[1] in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]): 297 shutil.rmtree(src) 298 299 #######then copy the selected files to their folders########################### 300 GlobalCount = 0 301 for File in List: 302 dest = os.path.join(SelectedDir, File) 303 src = os.path.join(config.DefaultRepository, File) 304 destdir = os.path.dirname(dest) 305 self.refreshSignal.emit(GlobalCount, File) 306 GlobalCount += 1 307 if not os.path.isdir(destdir): makedir(destdir) 308 if not os.path.exists(dest): 309 if filigrane: 310 Img = Image.open(src) 311 filigrane.substract(Img).save(dest, quality=config.FiligraneQuality, optimize=config.FiligraneOptimize, progressive=config.FiligraneOptimize) 312 else: 313 shutil.copy(src, dest) 314 os.chmod(dest, config.DefaultFileMode) 315 else : 316 print "%s existe déja" % (dest) 317 ######copy the comments of the directory to the Selected directory 318 AlreadyDone = [] 319 for File in List: 320 directory = os.path.split(File)[0] 321 if directory in AlreadyDone: 322 continue 323 else: 324 AlreadyDone.append(directory) 325 dst = os.path.join(SelectedDir, directory, config.CommentFile) 326 src = os.path.join(config.DefaultRepository, directory, config.CommentFile) 327 if os.path.isfile(src): 328 shutil.copy(src, dst) 329 self.finishSignal.emit()
330 331 332 333
334 -class ModelRangeTout:
335 """Implemantation MVC de la procedure RangeTout 336 moves all the JPEG files to a directory named from 337 their day and with the name according to the time""" 338
339 - def __init__(self):
340 """ 341 """ 342 self.__label = "Initial renaming of new images .... " 343 self.startSignal = Signal() 344 self.refreshSignal = Signal() 345 self.finishSignal = Signal() 346 self.NbrJobsSignal = Signal()
347 348
349 - def start(self, RootDir):
350 """ Lance les calculs 351 """ 352 config.DefaultRepository = RootDir 353 AllJpegs = FindFile(RootDir) 354 AllFilesToProcess = [] 355 AllreadyDone = [] 356 NewFiles = [] 357 uid = os.getuid() 358 gid = os.getgid() 359 for i in AllJpegs: 360 if i.find(config.TrashDirectory) == 0: continue 361 if i.find(config.SelectedDirectory) == 0: continue 362 try: 363 a = int(i[:4]) 364 m = int(i[5:7]) 365 j = int(i[8:10]) 366 if (a >= 0000) and (m <= 12) and (j <= 31) and (i[4] in ["-", "_", "."]) and (i[7] in ["-", "_"]): 367 AllreadyDone.append(i) 368 else: 369 AllFilesToProcess.append(i) 370 except ValueError: 371 AllFilesToProcess.append(i) 372 AllFilesToProcess.sort() 373 NumFiles = len(AllFilesToProcess) 374 self.startSignal.emit(self.__label, NumFiles) 375 for h in range(NumFiles): 376 i = AllFilesToProcess[h] 377 self.refreshSignal.emit(h, i) 378 data = photo(i).exif() 379 try: 380 datei, heurei = data["Heure"].split() 381 date = re.sub(":", "-", datei) 382 heurej = re.sub(":", "h", heurei, 1) 383 model = data["Modele"].split(",")[-1] 384 heure = latin1_to_ascii("%s-%s.jpg" % (re.sub(":", "m", heurej, 1), re.sub("/", "", re.sub(" ", "_", model)))) 385 except ValueError: 386 date = time.strftime("%Y-%m-%d", time.gmtime(os.path.getctime(os.path.join(RootDir, i)))) 387 heure = latin1_to_ascii("%s-%s.jpg" % (time.strftime("%Hh%Mm%S", time.gmtime(os.path.getctime(os.path.join(RootDir, i)))), re.sub("/", "-", re.sub(" ", "_", os.path.splitext(i)[0])))) 388 if not (os.path.isdir(os.path.join(RootDir, date))) : mkdir(os.path.join(RootDir, date)) 389 strImageFile = os.path.join(RootDir, date, heure) 390 ToProcess = os.path.join(date, heure) 391 if os.path.isfile(strImageFile): 392 print "Problème ... %s existe déja " % i 393 s = 0 394 for j in os.listdir(os.path.join(RootDir, date)): 395 if j.find(heure[:-4]) == 0:s += 1 396 ToProcess = os.path.join(date, heure[:-4] + "-%s.jpg" % s) 397 strImageFile = os.path.join(RootDir, ToProcess) 398 shutil.move(os.path.join(RootDir, i), strImageFile) 399 try: 400 os.chown(strImageFile, uid, gid) 401 os.chmod(strImageFile, config.DefaultFileMode) 402 except OSError: 403 print "error in chown or chmod of %s" % strImageFile 404 if config.AutoRotate and data["Orientation"] != "1": 405 photo(strImageFile).autorotate() 406 #Set the new images in cache for further display 407 if imageCache is not None: 408 if config.ImageWidth and config.ImageHeight: 409 if imageCache.size + 3 * config.ImageWidth * config.ImageHeight < imageCache.maxSize: 410 print "put in cache file " + ToProcess 411 pixbuf = gtk.gdk.pixbuf_new_from_file(strImageFile) 412 Xsize = pixbuf.get_width() 413 Ysize = pixbuf.get_height() 414 R = min(float(config.ImageWidth) / float(Xsize), float(config.ImageHeight) / float(Ysize)) 415 if R < 1: 416 scaled_buf = pixbuf.scale_simple(config.ImageWidth, config.ImageHeight, gtkInterpolation[config.Interpolation]) 417 else: 418 scaled_buf = pixbuf 419 imageCache[ ToProcess ] = scaled_buf 420 ################################################## 421 AllreadyDone.append(ToProcess) 422 NewFiles.append(ToProcess) 423 AllreadyDone.sort() 424 self.finishSignal.emit() 425 426 if len(NewFiles) > 0: 427 FirstImage = min(NewFiles) 428 return AllreadyDone, AllreadyDone.index(FirstImage) 429 else: 430 return AllreadyDone, 0
431
432 -class Controler:
433 """ Implémentation du contrôleur de la vue utilisant la console"""
434 - def __init__(self, model, view):
435 # self.__model = model # Ne sert pas ici, car on ne fait que des actions modèle -> vue 436 self.__view = view 437 438 # Connection des signaux 439 model.startSignal.connect(self.__startCallback) 440 model.refreshSignal.connect(self.__refreshCallback) 441 model.finishSignal.connect(self.__stopCallback) 442 model.NbrJobsSignal.connect(self.__NBJCallback)
443 - def __startCallback(self, label, nbVal):
444 """ Callback pour le signal de début de progressbar.""" 445 self.__view.creatProgressBar(label, nbVal)
446 - def __refreshCallback(self, i, filename):
447 """ Mise à jour de la progressbar.""" 448 self.__view.updateProgressBar(i, filename)
449 - def __stopCallback(self):
450 """ Callback pour le signal de fin de splashscreen.""" 451 self.__view.finish()
452 - def __NBJCallback(self, NbrJobs):
453 """ Callback pour redefinir le nombre de job totaux.""" 454 self.__view.ProgressBarMax(NbrJobs)
455 456 457
458 -class ControlerX:
459 """ Implémentation du contrôleur. C'est lui qui lie les modèle et la(les) vue(s)."""
460 - def __init__(self, model, viewx):
461 # self.__model = model # Ne sert pas ici, car on ne fait que des actions modèle -> vue 462 self.__viewx = viewx 463 # Connection des signaux 464 model.startSignal.connect(self.__startCallback) 465 model.refreshSignal.connect(self.__refreshCallback) 466 model.finishSignal.connect(self.__stopCallback) 467 model.NbrJobsSignal.connect(self.__NBJCallback)
468 - def __startCallback(self, label, nbVal):
469 """ Callback pour le signal de début de progressbar.""" 470 self.__viewx.creatProgressBar(label, nbVal)
471 - def __refreshCallback(self, i, filename):
472 """ Mise à jour de la progressbar. """ 473 self.__viewx.updateProgressBar(i, filename)
474 - def __stopCallback(self):
475 """ ferme la fenetre. Callback pour le signal de fin de splashscreen.""" 476 self.__viewx.finish()
477 - def __NBJCallback(self, NbrJobs):
478 """ Callback pour redefinir le nombre de job totaux.""" 479 self.__viewx.ProgressBarMax(NbrJobs)
480 481 482
483 -class View:
484 """ Implémentation de la vue. 485 Utilisation de la console. 486 """
487 - def __init__(self):
488 """ On initialise la vue.""" 489 self.__nbVal = None
490 - def creatProgressBar(self, label, nbVal):
491 """ Création de la progressbar. """ 492 self.__nbVal = nbVal 493 print label
494
495 - def ProgressBarMax(self, nbVal):
496 """re-definit le nombre maximum de la progress-bar""" 497 self.__nbVal = nbVal
498 # print "Modification du maximum : %i"%self.__nbVal 499
500 - def updateProgressBar(self, h, filename):
501 """ Mise à jour de la progressbar 502 """ 503 print "%5.1f %% processing ... %s" % (100.0 * (h + 1) / self.__nbVal, filename)
504 - def finish(self):
505 """nothin in text mode""" 506 pass
507
508 -class ViewX:
509 """ 510 Implementation of the view as a splashscren 511 """
512 - def __init__(self):
513 """ 514 Initialization of the view in the constructor 515 516 Ici, on ne fait rien, car la progressbar sera créée au moment 517 où on en aura besoin. Dans un cas réel, on initialise les widgets 518 de l'interface graphique 519 """ 520 self.__nbVal = None 521 self.xml = None 522 self.pb = None
523
524 - def creatProgressBar(self, label, nbVal):
525 """ 526 Creation of a progress bar. 527 """ 528 self.xml = GTKglade.XML(unifiedglade, root="splash") 529 self.xml.get_widget("image").set_from_pixbuf(gtk.gdk.pixbuf_new_from_file(os.path.join(installdir, "Splash.png"))) 530 self.pb = self.xml.get_widget("progress") 531 self.xml.get_widget("splash").set_title(label) 532 self.xml.get_widget("splash").show() 533 while gtk.events_pending():gtk.main_iteration() 534 self.__nbVal = nbVal
535 - def ProgressBarMax(self, nbVal):
536 """re-definit le nombre maximum de la progress-bar""" 537 self.__nbVal = nbVal
538
539 - def updateProgressBar(self, h, filename):
540 """ Mise à jour de la progressbar 541 Dans le cas d'un toolkit, c'est ici qu'il faudra appeler le traitement 542 des évènements. 543 set the progress-bar to the given value with the given name 544 @param h: current number of the file 545 @type val: integer or float 546 @param name: name of the current element 547 @type name: string 548 @return: None""" 549 if h < self.__nbVal: 550 self.pb.set_fraction(float(h + 1) / self.__nbVal) 551 else: 552 self.pb.set_fraction(1.0) 553 self.pb.set_text(filename) 554 while gtk.events_pending():gtk.main_iteration()
555 - def finish(self):
556 """destroys the interface of the splash screen""" 557 self.xml.get_widget("splash").destroy() 558 while gtk.events_pending():gtk.main_iteration() 559 del self.xml 560 gc.collect()
561 562
563 -def RangeTout(repository):
564 """moves all the JPEG files to a directory named from their day and with the 565 name according to the time 566 This is a MVC implementation""" 567 model = ModelRangeTout() 568 view = View() 569 Controler(model, view) 570 viewx = ViewX() 571 ControlerX(model, viewx) 572 return model.start(repository)
573
574 -def ProcessSelected(SelectedFiles):
575 """This procedure uses the MVC implementation of processSelected 576 It makes a copy of all selected photos and scales them 577 copy all the selected files to "selected" subdirectory, 20 per page 578 """ 579 print "execution %s" % SelectedFiles 580 model = ModelProcessSelected() 581 view = View() 582 Controler(model, view) 583 viewx = ViewX() 584 ControlerX(model, viewx) 585 model.start(SelectedFiles)
586
587 -def CopySelected(SelectedFiles):
588 """This procedure makes a copy of all selected photos and scales them 589 copy all the selected files to "selected" subdirectory 590 """ 591 print "Copy %s" % SelectedFiles 592 model = ModelCopySelected() 593 view = View() 594 Controler(model, view) 595 viewx = ViewX() 596 ControlerX(model, viewx) 597 model.start(SelectedFiles)
598 599 600 601 602 ########################################################## 603 # # # # # # Début de la classe photo # # # # # # # # # # # 604 ##########################################################
605 -class photo:
606 """class photo that does all the operations available on photos"""
607 - def __init__(self, filename):
608 self.filename = filename 609 self.fn = os.path.join(config.DefaultRepository, self.filename) 610 self.data = None 611 self.x = None 612 self.y = None 613 self.g = None 614 self.f = None 615 if not os.path.isfile(self.fn): 616 print "Erreur, le fichier %s n'existe pas" % self.fn 617 self.bImageCache = (imageCache is not None)
618
619 - def LoadPIL(self):
620 """Load the image""" 621 self.f = Image.open(self.fn)
622
623 - def larg(self):
624 """width-height of a jpeg file""" 625 self.taille() 626 return self.x - self.y
627
628 - def taille(self):
629 """width and height of a jpeg file""" 630 if self.x == None and self.y == None: 631 self.LoadPIL() 632 self.x, self.y = self.f.size
633
634 - def SaveThumb(self, Thumbname, Size=160, Interpolation=1, Quality=75, Progressive=False, Optimize=False, ExifExtraction=False):
635 """save a thumbnail of the given name, with the given size and the interpolation methode (quality) 636 resampling filters : 637 NONE = 0 638 NEAREST = 0 639 ANTIALIAS = 1 # 3-lobed lanczos 640 LINEAR = BILINEAR = 2 641 CUBIC = BICUBIC = 3 642 """ 643 if os.path.isfile(Thumbname): 644 print "sorry, file %s exists" % Thumbname 645 else: 646 image_exif = pyexiv2.Image(self.fn) 647 image_exif.readMetadata() 648 #RawExif,comment=EXIF.process_file(open(self.fn,'rb'),0) 649 extract = False 650 print "process file %s exists" % Thumbname 651 if ExifExtraction: 652 try: 653 image_exif.dumpThumbnailToFile(Thumbname[:-4]) 654 extract = True 655 except OSError: 656 extract = False 657 if not extract: 658 # print "on essaie avec PIL" 659 self.LoadPIL() 660 self.g = self.f.copy() 661 self.g.thumbnail((Size, Size), Interpolation) 662 self.g.save(Thumbname, quality=Quality, progressive=Progressive, optimize=Optimize) 663 os.chmod(Thumbname, config.DefaultFileMode)
664 665
666 - def Rotate(self, angle=0):
667 """does a looseless rotation of the given jpeg file""" 668 if os.name == 'nt' and self.f != None: del self.f 669 self.taille() 670 x = self.x 671 y = self.y 672 if angle == 90: 673 if imageCache is not None: 674 os.system('%s -ip -9 "%s" &' % (exiftran, self.fn)) 675 imageCache[self.filename] = imageCache[self.filename].rotate_simple(gtk.gdk.PIXBUF_ROTATE_CLOCKWISE) 676 self.x = y 677 self.y = x 678 else: 679 os.system('%s -ip -9 "%s" ' % (exiftran, self.fn)) 680 self.x = None 681 self.y = None 682 elif angle == 270: 683 if imageCache is not None: 684 os.system('%s -ip -2 "%s" &' % (exiftran, self.fn)) 685 imageCache[self.filename] = imageCache[self.filename].rotate_simple(gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) 686 self.x = y 687 self.y = x 688 else: 689 os.system('%s -ip -2 "%s" ' % (exiftran, self.fn)) 690 self.x = None 691 self.y = None 692 elif angle == 180: 693 if imageCache is not None: 694 os.system('%s -ip -1 "%s" &' % (exiftran, self.fn)) 695 imageCache[self.filename] = imageCache[self.filename].rotate_simple(gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN) 696 self.x = x 697 self.y = y 698 else: 699 os.system('%s -ip -1 "%s" ' % (exiftran, self.fn)) 700 self.x = None 701 self.y = None 702 else: 703 print "Erreur ! il n'est pas possible de faire une rotation de ce type sans perte de donnée."
704
705 - def RemoveFromCache(self):
706 """remove the curent image from the Cache .... for various reasons""" 707 if imageCache is not None: 708 if self.filename in imageCache.ordered: 709 pixBuf = imageCache.imageDict.pop(self.filename) 710 index = imageCache.ordered.index(self.filename) 711 imageCache.ordered.pop(index) 712 imageCache.size -= 3 * pixBuf.get_width() * pixBuf.get_height()
713
714 - def Trash(self):
715 """Send the file to the trash folder""" 716 self.RemoveFromCache() 717 Trashdir = os.path.join(config.DefaultRepository, config.TrashDirectory) 718 td = os.path.dirname(os.path.join(Trashdir, self.filename)) 719 #tf = os.path.join(Trashdir, self.filename) 720 if not os.path.isdir(td): makedir(td) 721 shutil.move(self.fn, os.path.join(Trashdir, self.filename))
722
723 - def exif(self):
724 """return exif data + title from the photo""" 725 clef = {'Exif.Image.Make':'Marque', 726 'Exif.Image.Model':'Modele', 727 'Exif.Photo.DateTimeOriginal':'Heure', 728 'Exif.Photo.ExposureTime':'Vitesse', 729 'Exif.Photo.FNumber':'Ouverture', 730 # 'Exif.Photo.DateTimeOriginal':'Heure2', 731 'Exif.Photo.ExposureBiasValue':'Bias', 732 'Exif.Photo.Flash':'Flash', 733 'Exif.Photo.FocalLength':'Focale', 734 'Exif.Photo.ISOSpeedRatings':'Iso' , 735 'Exif.Image.Orientation':'Orientation' 736 } 737 738 if self.data is None: 739 self.data = {} 740 self.data["Taille"] = "%.2f %s" % SmartSize(os.path.getsize(self.fn)) 741 image_exif = pyexiv2.Image(self.fn) 742 image_exif.readMetadata() 743 self.data["Titre"] = image_exif.getComment() 744 self.taille() 745 self.data["Resolution"] = "%s x %s " % (self.x, self.y) 746 self.data["Orientation"] = "1" 747 for i in clef: 748 try: 749 self.data[clef[i]] = image_exif.interpretedExifValue(i).decode(config.Coding) 750 except: 751 self.data[clef[i]] = "" 752 return self.data
753
754 - def has_title(self):
755 """return true if the image is entitled""" 756 if self.data == None: 757 self.exif() 758 if self.data["Titre"]: 759 return True 760 else: 761 return False
762 763
764 - def show(self, Xsize=600, Ysize=600):
765 """return a pixbuf to shows the image in a Gtk window""" 766 scaled_buf = None 767 if Xsize > config.ImageWidth : config.ImageWidth = Xsize 768 if Ysize > config.ImageHeight: config.ImageHeight = Ysize 769 self.taille() 770 R = min(float(Xsize) / self.x, float(Ysize) / self.y) 771 if R < 1: 772 nx = int(R * self.x) 773 ny = int(R * self.y) 774 else: 775 nx = self.x 776 ny = self.y 777 if imageCache is not None: 778 if self.filename in imageCache.ordered: 779 data = imageCache[ self.filename ] 780 if (data.get_width() == nx) and (data.get_height() == ny): 781 scaled_buf = data 782 if config.DEBUG: print("Sucessfully fetched %s from cache, cache size: %i images, %.3f MBytes" % (self.filename, len(imageCache.ordered), (imageCache.size / 1048576.0))) 783 elif (data.get_width() > nx) or (data.get_height() > ny): 784 if config.DEBUG:print("nx=%i,\tny=%i,\tw=%i,h=%i" % (nx, ny, data.get_width(), data.get_height())) 785 pixbuf = data 786 if config.DEBUG: print("Fetched data for %s have to be rescaled, cache size: %i images, %.3f MBytes" % (self.filename, len(imageCache.ordered), (imageCache.size / 1048576.0))) 787 scaled_buf = pixbuf.scale_simple(nx, ny, gtkInterpolation[config.Interpolation]) 788 if not scaled_buf: 789 pixbuf = gtk.gdk.pixbuf_new_from_file(self.fn) 790 if R < 1: 791 scaled_buf = pixbuf.scale_simple(nx, ny, gtkInterpolation[config.Interpolation]) 792 else : 793 scaled_buf = pixbuf 794 if imageCache is not None: 795 imageCache[ self.filename ] = scaled_buf 796 if config.DEBUG: print("Sucessfully cached %s, cache size: %i images, %.3f MBytes" % (self.filename, len(imageCache.ordered), (imageCache.size / 1048576.0))) 797 return scaled_buf
798
799 - def name(self, titre):
800 """write the title of the photo inside the description field, in the JPEG header""" 801 if os.name == 'nt' and self.f != None: del self.f 802 image_exif = pyexiv2.Image(self.fn) 803 image_exif.readMetadata() 804 image_exif.setComment(titre) 805 image_exif.writeMetadata()
806
807 - def autorotate(self):
808 """does autorotate the image according to the EXIF tag""" 809 if os.name == 'nt' and self.f != None: del self.f 810 os.system('%s -aip "%s"' % (exiftran, self.fn))
811
812 - def ContrastMask(self, outfile):
813 """Ceci est un filtre de debouchage de photographies, aussi appelé masque de contraste, il permet de rattrapper une photo trop contrasté, un contre jour, ... 814 Écrit par Jérôme Kieffer, avec l'aide de la liste python@aful, en particulier A. Fayolles et F. Mantegazza 815 avril 2006 816 necessite numpy et PIL.""" 817 try: 818 import numpy 819 except: 820 raise ImportError("This filter needs the numpy library available on https://sourceforge.net/projects/numpy/files/") 821 self.LoadPIL() 822 x, y = self.f.size 823 ImageFile.MAXBLOCK = x * y 824 img_array = numpy.fromstring(self.f.tostring(), dtype="UInt8").astype("UInt16") 825 img_array.shape = (y, x, 3) 826 red, green, blue = img_array[:, :, 0], img_array[:, :, 1], img_array[:, :, 2] 827 desat_array = (numpy.minimum(numpy.minimum(red, green), blue) + numpy.maximum(numpy.maximum(red, green), blue)) / 2 828 inv_desat = 255 - desat_array 829 k = Image.fromarray(inv_desat, "L").convert("RGB") 830 S = ImageChops.screen(self.f, k) 831 M = ImageChops.multiply(self.f, k) 832 F = ImageChops.add(ImageChops.multiply(self.f, S), ImageChops.multiply(ImageChops.invert(self.f), M)) 833 F.save(os.path.join(config.DefaultRepository, outfile), quality=90, progressive=True, Optimize=True) 834 os.chmod(os.path.join(config.DefaultRepository, outfile), config.DefaultFileMode)
835 836 ######################################################## 837 # # # # # # fin de la classe photo # # # # # # # # # # # 838 ######################################################## 839
840 -class signature:
841 - def __init__(self, filename):
842 """ 843 this filter allows add a signature to an image 844 """ 845 self.img = None 846 self.sig = Image.open(filename) 847 self.sig.convert("RGB") 848 (self.xs, self.ys) = self.sig.size 849 self.bigsig = self.sig 850 #The signature file is entented to be white on a black background, this inverts the color if necessary 851 if ImageStat.Stat(self.sig)._getmean() > 127: 852 self.sig = ImageChops.invert(self.sig) 853 854 self.orientation = -1 #this is an impossible value 855 (self.x, self.y) = (self.xs, self.ys)
856
857 - def mask(self, orientation=5):
858 """ 859 x and y are the size of the initial image 860 the orientation correspond to the position on a clock : 861 0 for the center 862 1 or 2 upper right 863 3 centered in heith right side ....""" 864 if orientation == self.orientation and (self.x, self.y) == self.bigsig.size: 865 #no need to change the mask 866 return 867 self.orientation = orientation 868 self.bigsig = Image.new("RGB", (self.x, self.y), (0, 0, 0)) 869 if self.x < self.xs or self.y < self.ys : 870 #the signature is larger than the image 871 return 872 if self.orientation == 0: 873 self.bigsig.paste(self.sig, (self.x / 2 - self.xs / 2, self.y / 2 - self.ys / 2, self.x / 2 - self.xs / 2 + self.xs, self.y / 2 - self.ys / 2 + self.ys)) 874 elif self.orientation in [1, 2]: 875 self.bigsig.paste(self.sig, (self.x - self.xs, 0, self.x, self.ys)) 876 elif self.orientation == 3: 877 self.bigsig.paste(self.sig, (self.x - self.xs, self.y / 2 - self.ys / 2, self.x, self.y / 2 - self.ys / 2 + self.ys)) 878 elif self.orientation in [ 5, 4]: 879 self.bigsig.paste(self.sig, (self.x - self.xs, self.y - self.ys, self.x, self.y)) 880 elif self.orientation == 6: 881 self.bigsig.paste(self.sig, (self.x / 2 - self.xs / 2, self.y - self.ys, self.x / 2 - self.xs / 2 + self.xs, self.y)) 882 elif self.orientation in [7, 8]: 883 self.bigsig.paste(self.sig, (0, self.y - self.ys, self.xs, self.y)) 884 elif self.orientation == 9: 885 self.bigsig.paste(self.sig, (0, self.y / 2 - self.ys / 2, self.xs, self.y / 2 - self.ys / 2 + self.ys)) 886 elif self.orientation in [10, 11]: 887 self.bigsig.paste(self.sig, (0, 0, self.xs, self.ys)) 888 elif self.orientation == 12: 889 self.bigsig.paste(self.sig, (self.x / 2 - self.xs / 2, 0, self.x / 2 - self.xs / 2 + self.xs, self.ys)) 890 return
891
892 - def substract(self, inimage, orientation=5):
893 """apply a substraction mask on the image""" 894 self.img = inimage 895 self.x, self.y = self.img.size 896 ImageFile.MAXBLOCK = self.x * self.y 897 self.mask(orientation) 898 k = ImageChops.difference(self.img, self.bigsig) 899 return k
900 901 902 903 ############################################################################################################ 904
905 -def makedir(filen):
906 """creates the tree structure for the file""" 907 dire = os.path.dirname(filen) 908 if os.path.isdir(dire): 909 mkdir(filen) 910 else: 911 makedir(dire) 912 mkdir(filen)
913
914 -def mkdir(filename):
915 """create an empty directory with the given rights""" 916 # config=Config() 917 os.mkdir(filename) 918 os.chmod(filename, config.DefaultDirMode)
919 920
921 -def FindFile(RootDir):
922 """returns a list of the files with the given suffix in the given dir 923 files=os.system('find "%s" -iname "*.%s"'%(RootDir,suffix)).readlines() 924 """ 925 files = [] 926 # config=Config() 927 for i in config.Extensions: 928 files += parser().FindExts(RootDir, i) 929 good = [] 930 l = len(RootDir) + 1 931 for i in files: good.append(i.strip()[l:]) 932 good.sort() 933 return good
934 935 936 #######################################################################################
937 -def ScaleImage(filename, filigrane=None):
938 """common processing for one image : create a subfolder "scaled" and "thumb" : """ 939 # config=Config() 940 rootdir = os.path.dirname(filename) 941 scaledir = os.path.join(rootdir, config.ScaledImages["Suffix"]) 942 thumbdir = os.path.join(rootdir, config.Thumbnails["Suffix"]) 943 if not os.path.isdir(scaledir) : mkdir(scaledir) 944 if not os.path.isdir(thumbdir) : mkdir(thumbdir) 945 Img = photo(filename) 946 Param = config.ScaledImages.copy() 947 Param.pop("Suffix") 948 Param["Thumbname"] = os.path.join(scaledir, os.path.basename(filename))[:-4] + "--%s.jpg" % config.ScaledImages["Suffix"] 949 Img.SaveThumb(**Param) 950 Param = config.Thumbnails.copy() 951 Param.pop("Suffix") 952 Param["Thumbname"] = os.path.join(thumbdir, os.path.basename(filename))[:-4] + "--%s.jpg" % config.Thumbnails["Suffix"] 953 Img.SaveThumb(**Param) 954 if filigrane: 955 filigrane.substract(Img.f).save(filename, quality=config.FiligraneQuality, optimize=config.FiligraneOptimize, progressive=config.FiligraneOptimize) 956 os.chmod(filename, config.DefaultFileMode)
957 958 959 960 961 962
963 -def latin1_to_ascii (unicrap):
964 """This takes a UNICODE string and replaces Latin-1 characters with 965 something equivalent in 7-bit ASCII. It returns a plain ASCII string. 966 This function makes a best effort to convert Latin-1 characters into 967 ASCII equivalents. It does not just strip out the Latin-1 characters. 968 All characters in the standard 7-bit ASCII range are preserved. 969 In the 8th bit range all the Latin-1 accented letters are converted 970 to unaccented equivalents. Most symbol characters are converted to 971 something meaningful. Anything not converted is deleted. 972 """ 973 xlate = {0xc0:'A', 0xc1:'A', 0xc2:'A', 0xc3:'A', 0xc4:'A', 0xc5:'A', 974 0xc6:'Ae', 0xc7:'C', 975 0xc8:'E', 0xc9:'E', 0xca:'E', 0xcb:'E', 976 0xcc:'I', 0xcd:'I', 0xce:'I', 0xcf:'I', 977 0xd0:'Th', 0xd1:'N', 978 0xd2:'O', 0xd3:'O', 0xd4:'O', 0xd5:'O', 0xd6:'O', 0xd8:'O', 979 0xd9:'U', 0xda:'U', 0xdb:'U', 0xdc:'U', 980 0xdd:'Y', 0xde:'th', 0xdf:'ss', 981 0xe0:'a', 0xe1:'a', 0xe2:'a', 0xe3:'a', 0xe4:'a', 0xe5:'a', 982 0xe6:'ae', 0xe7:'c', 983 0xe8:'e', 0xe9:'e', 0xea:'e', 0xeb:'e', 984 0xec:'i', 0xed:'i', 0xee:'i', 0xef:'i', 985 0xf0:'th', 0xf1:'n', 986 0xf2:'o', 0xf3:'o', 0xf4:'o', 0xf5:'o', 0xf6:'o', 0xf8:'o', 987 0xf9:'u', 0xfa:'u', 0xfb:'u', 0xfc:'u', 988 0xfd:'y', 0xfe:'th', 0xff:'y', 989 0xa1:'!', 0xa2:'{cent}', 0xa3:'{pound}', 0xa4:'{currency}', 990 0xa5:'{yen}', 0xa6:'|', 0xa7:'{section}', 0xa8:'{umlaut}', 991 0xa9:'{C}', 0xaa:'{^a}', 0xab:'<<', 0xac:'{not}', 992 0xad:'-', 0xae:'{R}', 0xaf:'_', 0xb0:'{degrees}', 993 0xb1:'{+/-}', 0xb2:'{^2}', 0xb3:'{^3}', 0xb4:"'", 994 0xb5:'{micro}', 0xb6:'{paragraph}', 0xb7:'*', 0xb8:'{cedilla}', 995 0xb9:'{^1}', 0xba:'{^o}', 0xbb:'>>', 996 0xbc:'{1/4}', 0xbd:'{1/2}', 0xbe:'{3/4}', 0xbf:'?', 997 0xd7:'*', 0xf7:'/' 998 } 999 1000 r = [] 1001 for i in unicrap: 1002 if xlate.has_key(ord(i)): 1003 r.append(xlate[ord(i)]) 1004 elif ord(i) >= 0x80: 1005 pass 1006 else: 1007 r.append(str(i)) 1008 return "".join(r)
1009
1010 -def SmartSize(size):
1011 """print the size of files in a pretty way""" 1012 unit = "o" 1013 fsize = float(size) 1014 if len(str(size)) > 3: 1015 size /= 1024 1016 fsize /= 1024.0 1017 unit = "ko" 1018 if len(str(size)) > 3: 1019 size = size / 1024 1020 fsize /= 1024.0 1021 unit = "Mo" 1022 if len(str(size)) > 3: 1023 size = size / 1024 1024 fsize /= 1024.0 1025 unit = "Go" 1026 return fsize, unit
1027 1028 1029 ################################################################################ 1030 # We should refactorize this with os.walk !!! 1031 ################################################################################
1032 -class parser:
1033 """this class searches all the jpeg files"""
1034 - def __init__(self):
1035 self.imagelist = [] 1036 self.root = None 1037 self.suffix = None
1038
1039 - def OneDir(self, curent):
1040 """ append all the imagesfiles to the list, then goes recursively to the subdirectories""" 1041 ls = os.listdir(curent) 1042 for i in ls: 1043 a = os.path.join(curent, i) 1044 if os.path.isdir(a): 1045 self.OneDir(a) 1046 if os.path.isfile(a): 1047 if i[(-len(self.suffix)):].lower() == self.suffix: 1048 self.imagelist.append(os.path.join(curent, i))
1049 - def FindExts(self, root, suffix):
1050 self.root = root 1051 self.suffix = suffix 1052 self.OneDir(self.root) 1053 return self.imagelist
1054 1055
1056 -def recursive_delete(dirname):
1057 files = os.listdir(dirname) 1058 for filename in files: 1059 path = os.path.join (dirname, filename) 1060 if os.path.isdir(path): 1061 recursive_delete(path) 1062 else: 1063 print 'Removing file: "%s"' % path 1064 os.remove(path) 1065 1066 print 'Removing directory:', dirname 1067 os.rmdir(dirname)
1068 1069 1070 1071 1072 if __name__ == "__main__": 1073 #################################################################################### 1074 #Definition de la classe des variables de configuration globales : Borg""" 1075 config.DefaultRepository = os.path.abspath(sys.argv[1]) 1076 print config.DefaultRepository 1077 RangeTout(sys.argv[1]) 1078