1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
45 gtkInterpolation = [gtk.gdk.INTERP_NEAREST, gtk.gdk.INTERP_TILES, gtk.gdk.INTERP_BILINEAR, gtk.gdk.INTERP_HYPER]
46
47
48
49
50
51
52
53 installdir = os.path.join(distutils.sysconfig.get_python_lib(), "imagizer")
54 if os.name == 'nt':
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
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
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
106 """Implemantation MVC de la procedure ProcessSelected"""
108 """
109 """
110 self.__label = "Un moment..."
111 self.startSignal = Signal()
112 self.refreshSignal = Signal()
113 self.finishSignal = Signal()
114 self.NbrJobsSignal = Signal()
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
169 AlsoProcess = 0
170 for day in os.listdir(SelectedDir):
171
172 DayOrFile = os.path.join(SelectedDir, day)
173 if os.path.isfile(DayOrFile):
174 ArrangeOneFile(SelectedDir, day)
175 AlsoProcess += 1
176
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:
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
186
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
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
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
232 dirs = os.listdir(SelectedDir)
233 dirs.sort()
234
235 if config.ExportSingleDir:
236
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:
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
265 """Implemantation MVC de la procedure CopySelected"""
267 """
268 """
269 self.__label = "Un moment..."
270 self.startSignal = Signal()
271 self.refreshSignal = Signal()
272 self.finishSignal = Signal()
273 self.NbrJobsSignal = Signal()
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
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
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
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
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
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
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
433 """ Implémentation du contrôleur de la vue utilisant la console"""
444 """ Callback pour le signal de début de progressbar."""
445 self.__view.creatProgressBar(label, nbVal)
447 """ Mise à jour de la progressbar."""
448 self.__view.updateProgressBar(i, filename)
450 """ Callback pour le signal de fin de splashscreen."""
451 self.__view.finish()
453 """ Callback pour redefinir le nombre de job totaux."""
454 self.__view.ProgressBarMax(NbrJobs)
455
456
457
459 """ Implémentation du contrôleur. C'est lui qui lie les modèle et la(les) vue(s)."""
469 """ Callback pour le signal de début de progressbar."""
470 self.__viewx.creatProgressBar(label, nbVal)
472 """ Mise à jour de la progressbar. """
473 self.__viewx.updateProgressBar(i, filename)
475 """ ferme la fenetre. Callback pour le signal de fin de splashscreen."""
476 self.__viewx.finish()
478 """ Callback pour redefinir le nombre de job totaux."""
479 self.__viewx.ProgressBarMax(NbrJobs)
480
481
482
484 """ Implémentation de la vue.
485 Utilisation de la console.
486 """
488 """ On initialise la vue."""
489 self.__nbVal = None
491 """ Création de la progressbar. """
492 self.__nbVal = nbVal
493 print label
494
496 """re-definit le nombre maximum de la progress-bar"""
497 self.__nbVal = nbVal
498
499
501 """ Mise à jour de la progressbar
502 """
503 print "%5.1f %% processing ... %s" % (100.0 * (h + 1) / self.__nbVal, filename)
505 """nothin in text mode"""
506 pass
507
509 """
510 Implementation of the view as a splashscren
511 """
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
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
536 """re-definit le nombre maximum de la progress-bar"""
537 self.__nbVal = nbVal
538
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()
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
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
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
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
604
606 """class photo that does all the operations available on photos"""
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
620 """Load the image"""
621 self.f = Image.open(self.fn)
622
624 """width-height of a jpeg file"""
625 self.taille()
626 return self.x - self.y
627
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
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
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
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
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
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
720 if not os.path.isdir(td): makedir(td)
721 shutil.move(self.fn, os.path.join(Trashdir, self.filename))
722
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
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
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
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
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
838
839
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
851 if ImageStat.Stat(self.sig)._getmean() > 127:
852 self.sig = ImageChops.invert(self.sig)
853
854 self.orientation = -1
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
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
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
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
915 """create an empty directory with the given rights"""
916
917 os.mkdir(filename)
918 os.chmod(filename, config.DefaultDirMode)
919
920
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
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
938 """common processing for one image : create a subfolder "scaled" and "thumb" : """
939
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
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
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
1031
1033 """this class searches all the jpeg files"""
1035 self.imagelist = []
1036 self.root = None
1037 self.suffix = None
1038
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))
1050 self.root = root
1051 self.suffix = suffix
1052 self.OneDir(self.root)
1053 return self.imagelist
1054
1055
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
1075 config.DefaultRepository = os.path.abspath(sys.argv[1])
1076 print config.DefaultRepository
1077 RangeTout(sys.argv[1])
1078