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
33
34 from exiftran import Exiftran
35
36 try:
37 import Image, ImageStat, ImageChops, ImageFile
38 except:
39 raise ImportError("Selector needs PIL: Python Imaging Library\n PIL is available from http://www.pythonware.com/products/pil/")
40 try:
41 import pygtk ; pygtk.require('2.0')
42 import gtk
43 import gtk.glade as GTKglade
44 except ImportError:
45 raise ImportError("Selector needs pygtk and glade-2 available from http://www.pygtk.org/")
46
47 gtkInterpolation = [gtk.gdk.INTERP_NEAREST, gtk.gdk.INTERP_TILES, gtk.gdk.INTERP_BILINEAR, gtk.gdk.INTERP_HYPER]
48
49
50
51
52
53
54
55 installdir = os.path.dirname(__file__)
56 if os.name == 'nt':
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 ConfFile = ["/etc/imagizer.conf", os.path.join(os.getenv("HOME"), ".imagizer"), ".imagizer"]
60
61 unifiedglade = os.path.join(installdir, "selector.glade")
62 from signals import Signal
63 from config import Config
64 config = Config()
65 config.load(ConfFile)
66 if config.ImageCache > 1:
67 import imagecache
68 imageCache = imagecache.ImageCache(maxSize=config.ImageCache)
69 else:
70 imageCache = None
71 import pyexiv2
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
100 """Implemantation MVC de la procedure ProcessSelected"""
102 """
103 """
104 self.__label = "Un moment..."
105 self.startSignal = Signal()
106 self.refreshSignal = Signal()
107 self.finishSignal = Signal()
108 self.NbrJobsSignal = Signal()
110 """ Lance les calculs
111 """
112
113 def SplitIntoPages(pathday, GlobalCount):
114 """Split a directory (pathday) into pages of 20 images"""
115 files = []
116 for i in os.listdir(pathday):
117 if os.path.splitext(i)[1] in config.Extensions:files.append(i)
118 files.sort()
119 if len(files) > config.NbrPerPage:
120 pages = 1 + (len(files) - 1) / config.NbrPerPage
121 for i in range(1, pages + 1):
122 folder = os.path.join(pathday, config.PagePrefix + str(i))
123 if not os.path.isdir(folder): mkdir(folder)
124 for j in range(len(files)):
125 i = 1 + (j) / config.NbrPerPage
126 filename = os.path.join(pathday, config.PagePrefix + str(i), files[j])
127 self.refreshSignal.emit(GlobalCount, files[j])
128 GlobalCount += 1
129 shutil.move(os.path.join(pathday, files[j]), filename)
130 ScaleImage(filename, filigrane)
131 else:
132 for j in files:
133 self.refreshSignal.emit(GlobalCount, j)
134 GlobalCount += 1
135 ScaleImage(os.path.join(pathday, j), filigrane)
136 return GlobalCount
137 def ArrangeOneFile(dirname, filename):
138 try:
139 timetuple = time.strptime(filename[:19], "%Y-%m-%d_%Hh%Mm%S")
140 suffix = filename[19:]
141 except ValueError:
142 try:
143 timetuple = time.strptime(filename[:11], "%Y-%m-%d_")
144 suffix = filename[11:]
145 except ValueError:
146 print("Unable to handle such file: %s" % filename)
147 return
148 daydir = os.path.join(SelectedDir, time.strftime("%Y-%m-%d", timetuple))
149 if not os.path.isdir(daydir):
150 os.mkdir(daydir)
151 shutil.move(os.path.join(dirname, filename), os.path.join(daydir, time.strftime("%Hh%Mm%S", timetuple) + suffix))
152
153 self.startSignal.emit(self.__label, max(1, len(List)))
154 if config.Filigrane:
155 filigrane = signature(config.FiligraneSource)
156 else:
157 filigrane = None
158
159 SelectedDir = os.path.join(config.DefaultRepository, config.SelectedDirectory)
160 self.refreshSignal.emit(-1, "copie des fichiers existants")
161 if not os.path.isdir(SelectedDir): mkdir(SelectedDir)
162
163 AlsoProcess = 0
164 for day in os.listdir(SelectedDir):
165
166 DayOrFile = os.path.join(SelectedDir, day)
167 if os.path.isfile(DayOrFile):
168 ArrangeOneFile(SelectedDir, day)
169 AlsoProcess += 1
170
171 elif os.path.isdir(DayOrFile):
172 if day in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]:
173 recursive_delete(DayOrFile)
174 elif day.find(config.PagePrefix) == 0:
175 for File in os.listdir(DayOrFile):
176 if os.path.isfile(os.path.join(DayOrFile, File)):
177 ArrangeOneFile(DayOrFile, File)
178 AlsoProcess += 1
179
180
181 recursive_delete(DayOrFile)
182 else:
183 for File in os.listdir(DayOrFile):
184 if File.find(config.PagePrefix) == 0:
185 if os.path.isdir(os.path.join(SelectedDir, day, File)):
186 for strImageFile in os.listdir(os.path.join(SelectedDir, day, File)):
187 src = os.path.join(SelectedDir, day, File, strImageFile)
188 dst = os.path.join(SelectedDir, day, strImageFile)
189 if os.path.isfile(src) and not os.path.exists(dst):
190 shutil.move(src, dst)
191 AlsoProcess += 1
192 if (os.path.isdir(src)) and (os.path.split(src)[1] in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]):
193 shutil.rmtree(src)
194 else:
195 if os.path.splitext(File)[1] in config.Extensions:
196 AlsoProcess += 1
197
198
199 for File in List:
200 dest = os.path.join(SelectedDir, File)
201 src = os.path.join(config.DefaultRepository, File)
202 destdir = os.path.dirname(dest)
203 if not os.path.isdir(destdir): makedir(destdir)
204 if not os.path.exists(dest):
205 print "copie de %s " % (File)
206 shutil.copy(src, dest)
207 try:
208 os.chmod(dest, config.DefaultFileMode)
209 except OSError:
210 print("Warning: unable to chmod %s" % dest)
211 AlsoProcess += 1
212 else :
213 print "%s existe déja" % (dest)
214 if AlsoProcess > 0:self.NbrJobsSignal.emit(AlsoProcess)
215
216 AlreadyDone = []
217 for File in List:
218 directory = os.path.split(File)[0]
219 if directory in AlreadyDone:
220 continue
221 else:
222 AlreadyDone.append(directory)
223 dst = os.path.join(SelectedDir, directory, config.CommentFile)
224 src = os.path.join(config.DefaultRepository, directory, config.CommentFile)
225 if os.path.isfile(src):
226 shutil.copy(src, dst)
227
228
229 dirs = os.listdir(SelectedDir)
230 dirs.sort()
231
232 if config.ExportSingleDir:
233
234 for day in dirs:
235 daydir = os.path.join(SelectedDir, day)
236 for filename in os.listdir(daydir):
237 try:
238 timetuple = time.strptime(day[:10] + "_" + filename[:8], "%Y-%m-%d_%Hh%Mm%S")
239 suffix = filename[8:]
240 except ValueError:
241 try:
242 timetuple = time.strptime(day[:10], "%Y-%m-%d")
243 suffix = filename
244 except ValueError:
245 print ("Unable to handle dir: %s\t file: %s" % (day, filename))
246 continue
247 src = os.path.join(daydir, filename)
248 dst = os.path.join(SelectedDir, time.strftime("%Y-%m-%d_%Hh%Mm%S", timetuple) + suffix)
249 shutil.move(src, dst)
250 recursive_delete(daydir)
251 SplitIntoPages(SelectedDir, 0)
252 else:
253 GlobalCount = 0
254 for day in dirs:
255 GlobalCount = SplitIntoPages(os.path.join(SelectedDir, day), GlobalCount)
256
257 self.finishSignal.emit()
258
259
260
262 """Implemantation MVC de la procedure CopySelected"""
264 """
265 """
266 self.__label = "Un moment..."
267 self.startSignal = Signal()
268 self.refreshSignal = Signal()
269 self.finishSignal = Signal()
270 self.NbrJobsSignal = Signal()
272 """ Lance les calculs
273 """
274 self.startSignal.emit(self.__label, max(1, len(List)))
275 if config.Filigrane:
276 filigrane = signature(config.FiligraneSource)
277 else:
278 filigrane = None
279
280 SelectedDir = os.path.join(config.DefaultRepository, config.SelectedDirectory)
281 self.refreshSignal.emit(-1, "copie des fichiers existants")
282 if not os.path.isdir(SelectedDir): mkdir(SelectedDir)
283
284 for day in os.listdir(SelectedDir):
285 for File in os.listdir(os.path.join(SelectedDir, day)):
286 if File.find(config.PagePrefix) == 0:
287 if os.path.isdir(os.path.join(SelectedDir, day, File)):
288 for strImageFile in os.listdir(os.path.join(SelectedDir, day, File)):
289 src = os.path.join(SelectedDir, day, File, strImageFile)
290 dst = os.path.join(SelectedDir, day, strImageFile)
291 if os.path.isfile(src) and not os.path.exists(dst):
292 shutil.move(src, dst)
293 if (os.path.isdir(src)) and (os.path.split(src)[1] in [config.ScaledImages["Suffix"], config.Thumbnails["Suffix"]]):
294 shutil.rmtree(src)
295
296
297 GlobalCount = 0
298 for File in List:
299 dest = os.path.join(SelectedDir, File)
300 src = os.path.join(config.DefaultRepository, File)
301 destdir = os.path.dirname(dest)
302 self.refreshSignal.emit(GlobalCount, File)
303 GlobalCount += 1
304 if not os.path.isdir(destdir): makedir(destdir)
305 if not os.path.exists(dest):
306 if filigrane:
307 Img = Image.open(src)
308 filigrane.substract(Img).save(dest, quality=config.FiligraneQuality, optimize=config.FiligraneOptimize, progressive=config.FiligraneOptimize)
309 else:
310 shutil.copy(src, dest)
311 try:
312 os.chmod(dest, config.DefaultFileMode)
313 except OSError:
314 print("Warning: unable to chmod %s" % dest)
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 = findFiles(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 myPhoto = photo(i)
379 try:
380 imageCache[i] = myPhoto
381 except:
382 pass
383 data = myPhoto.readExif()
384 try:
385 datei, heurei = data["Heure"].split()
386 date = re.sub(":", "-", datei)
387 heurej = re.sub(":", "h", heurei, 1)
388 model = data["Modele"].split(",")[-1]
389 heure = unicode_to_ascii("%s-%s.jpg" % (re.sub(":", "m", heurej, 1), re.sub("/", "", re.sub(" ", "_", model))))
390 except ValueError:
391 date = time.strftime("%Y-%m-%d", time.gmtime(os.path.getctime(os.path.join(RootDir, i))))
392 heure = unicode_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]))))
393 if not (os.path.isdir(os.path.join(RootDir, date))) : mkdir(os.path.join(RootDir, date))
394 strImageFile = os.path.join(RootDir, date, heure)
395 ToProcess = os.path.join(date, heure)
396 if os.path.isfile(strImageFile):
397 print "Problème ... %s existe déja " % i
398 s = 0
399 for j in os.listdir(os.path.join(RootDir, date)):
400 if j.find(heure[:-4]) == 0:s += 1
401 ToProcess = os.path.join(date, heure[:-4] + "-%s.jpg" % s)
402 strImageFile = os.path.join(RootDir, ToProcess)
403 shutil.move(os.path.join(RootDir, i), strImageFile)
404 try:
405 os.chown(strImageFile, uid, gid)
406 os.chmod(strImageFile, config.DefaultFileMode)
407 except OSError:
408 print "Warning: unable to chown ot chmod %s" % strImageFile
409 myPhoto = photo(strImageFile)
410
411 myPhoto.storeOriginalName(i)
412
413 if config.AutoRotate:
414 myPhoto.autorotate()
415
416
417 try:
418 imageCache[ ToProcess ] = myPhoto
419 except:
420 pass
421
422 AllreadyDone.append(ToProcess)
423 NewFiles.append(ToProcess)
424 AllreadyDone.sort()
425 self.finishSignal.emit()
426
427 if len(NewFiles) > 0:
428 FirstImage = min(NewFiles)
429 return AllreadyDone, AllreadyDone.index(FirstImage)
430 else:
431 return AllreadyDone, 0
432
434 """ Implémentation du contrôleur de la vue utilisant la console"""
445 """ Callback pour le signal de début de progressbar."""
446 self.__view.creatProgressBar(label, nbVal)
448 """ Mise à jour de la progressbar."""
449 self.__view.updateProgressBar(i, filename)
451 """ Callback pour le signal de fin de splashscreen."""
452 self.__view.finish()
454 """ Callback pour redefinir le nombre de job totaux."""
455 self.__view.ProgressBarMax(NbrJobs)
456
457
458
460 """ Implémentation du contrôleur. C'est lui qui lie les modèle et la(les) vue(s)."""
470 """ Callback pour le signal de début de progressbar."""
471 self.__viewx.creatProgressBar(label, nbVal)
473 """ Mise à jour de la progressbar. """
474 self.__viewx.updateProgressBar(i, filename)
476 """ ferme la fenetre. Callback pour le signal de fin de splashscreen."""
477 self.__viewx.finish()
479 """ Callback pour redefinir le nombre de job totaux."""
480 self.__viewx.ProgressBarMax(NbrJobs)
481
482
483
485 """ Implémentation de la vue.
486 Utilisation de la console.
487 """
489 """ On initialise la vue."""
490 self.__nbVal = None
492 """ Création de la progressbar. """
493 self.__nbVal = nbVal
494 print label
495
497 """re-definit le nombre maximum de la progress-bar"""
498 self.__nbVal = nbVal
499
500
502 """ Mise à jour de la progressbar
503 """
504 print "%5.1f %% processing ... %s" % (100.0 * (h + 1) / self.__nbVal, filename)
506 """nothin in text mode"""
507 pass
508
510 """
511 Implementation of the view as a splashscren
512 """
514 """
515 Initialization of the view in the constructor
516
517 Ici, on ne fait rien, car la progressbar sera créée au moment
518 où on en aura besoin. Dans un cas réel, on initialise les widgets
519 de l'interface graphique
520 """
521 self.__nbVal = None
522 self.xml = None
523 self.pb = None
524
526 """
527 Creation of a progress bar.
528 """
529 self.xml = GTKglade.XML(unifiedglade, root="splash")
530 self.xml.get_widget("image").set_from_pixbuf(gtk.gdk.pixbuf_new_from_file(os.path.join(installdir, "Splash.png")))
531 self.pb = self.xml.get_widget("progress")
532 self.xml.get_widget("splash").set_title(label)
533 self.xml.get_widget("splash").show()
534 while gtk.events_pending():gtk.main_iteration()
535 self.__nbVal = nbVal
536
538 """re-definit le nombre maximum de la progress-bar"""
539 self.__nbVal = nbVal
540
541
543 """
544 Update the progress-bar to the given value with the given filename writen on it
545
546 fr: Mise à jour de la progressbar
547 fr: Dans le cas d'un toolkit, c'est ici qu'il faudra appeler le traitement
548 fr:des évènements.
549
550 @param h: current number of the file
551 @type h: integer or float
552 @param filename: name of the current element
553 @type filename: string
554 @return: None
555 """
556 if h < self.__nbVal:
557 self.pb.set_fraction(float(h + 1) / self.__nbVal)
558 else:
559 self.pb.set_fraction(1.0)
560 self.pb.set_text(filename)
561 while gtk.events_pending():gtk.main_iteration()
563 """destroys the interface of the splash screen"""
564 self.xml.get_widget("splash").destroy()
565 while gtk.events_pending():gtk.main_iteration()
566 del self.xml
567 gc.collect()
568
569
571 """moves all the JPEG files to a directory named from their day and with the
572 name according to the time
573 This is a MVC implementation
574
575 @param repository: the name of the starting repository
576 @type repository: string
577 @param bUseX: set to False to disable the use of the graphical splash screen
578 @type bUseX: boolean
579 """
580 model = ModelRangeTout()
581 view = View()
582 Controler(model, view)
583 if bUseX:
584 viewx = ViewX()
585 ControlerX(model, viewx)
586 return model.start(repository)
587
588
589
591 """This procedure uses the MVC implementation of processSelected
592 It makes a copy of all selected photos and scales them
593 copy all the selected files to "selected" subdirectory, 20 per page
594 """
595 print "execution %s" % SelectedFiles
596 model = ModelProcessSelected()
597 view = View()
598 Controler(model, view)
599 viewx = ViewX()
600 ControlerX(model, viewx)
601 model.start(SelectedFiles)
602
604 """This procedure makes a copy of all selected photos and scales them
605 copy all the selected files to "selected" subdirectory
606 """
607 print "Copy %s" % SelectedFiles
608 model = ModelCopySelected()
609 view = View()
610 Controler(model, view)
611 viewx = ViewX()
612 ControlerX(model, viewx)
613 model.start(SelectedFiles)
614
615
616
617
618
619
620
622 """class photo that does all the operations available on photos"""
623 GaussianKernel = None
624
626 self.filename = filename
627 self.fn = os.path.join(config.DefaultRepository, self.filename)
628 self.metadata = None
629 self.pixelsX = None
630 self.pixelsY = None
631 self.pil = None
632 self.exif = None
633 if not os.path.isfile(self.fn):
634 print "Erreur, le fichier %s n'existe pas" % self.fn
635
636 self.scaledPixbuffer = None
637 self.orientation = 1
638
640 """Load the image"""
641 self.pil = Image.open(self.fn)
642
644 """width-height of a jpeg file"""
645 self.taille()
646 return self.pixelsX - self.pixelsY
647
649 """width and height of a jpeg file"""
650 if self.pixelsX == None and self.pixelsY == None:
651 self.LoadPIL()
652 self.pixelsX, self.pixelsY = self.pil.size
653
654 - def SaveThumb(self, strThumbFile, Size=160, Interpolation=1, Quality=75, Progressive=False, Optimize=False, ExifExtraction=False):
655 """save a thumbnail of the given name, with the given size and the interpolation methode (quality)
656 resampling filters :
657 NONE = 0
658 NEAREST = 0
659 ANTIALIAS = 1 # 3-lobed lanczos
660 LINEAR = BILINEAR = 2
661 CUBIC = BICUBIC = 3
662 """
663 if os.path.isfile(strThumbFile):
664 print "sorry, file %s exists" % strThumbFile
665 else:
666 if self.exif is None:
667 self.exif = pyexiv2.Image(self.fn)
668 self.exif.readMetadata()
669 extract = False
670 print "process file %s exists" % strThumbFile
671 if ExifExtraction:
672 try:
673 self.exif.dumpThumbnailToFile(strThumbFile[:-4])
674 extract = True
675 except (OSError, IOError):
676 extract = False
677
678 if os.path.isfile(strThumbFile):
679 thumbImag = photo(strThumbFile)
680 if self.larg()*thumbImag.larg() < 0:
681 print("Warning: thumbnail was not with the same orientation as original: %s" % self.filename)
682 os.remove(strThumbFile)
683 extract = False
684 if not extract:
685
686 if self.pil is None:
687 self.LoadPIL()
688 copyOfImage = self.pil.copy()
689 copyOfImage.thumbnail((Size, Size), Interpolation)
690 copyOfImage.save(strThumbFile, quality=Quality, progressive=Progressive, optimize=Optimize)
691 try:
692 os.chmod(strThumbFile, config.DefaultFileMode)
693 except OSError:
694 print("Warning: unable to chmod %s" % strThumbFile)
695
696
698 """does a looseless rotation of the given jpeg file"""
699 if os.name == 'nt' and self.pil != None:
700 del self.pil
701 self.taille()
702 x = self.pixelsX
703 y = self.pixelsY
704 if config.DEBUG:
705 print("Before rotation %i, x=%i, y=%i, scaledX=%i, scaledY=%i" % (angle, x, y, self.scaledPixbuffer.get_width(), self.scaledPixbuffer.get_height()))
706
707 if angle == 90:
708 if imageCache is not None:
709 Exiftran.rotate90(self.fn)
710
711 newPixbuffer = self.scaledPixbuffer.rotate_simple(gtk.gdk.PIXBUF_ROTATE_CLOCKWISE)
712 self.pixelsX = y
713 self.pixelsY = x
714 self.metadata["Resolution"] = "%i x % i" % (y, x)
715 else:
716 Exiftran.rotate90(self.fn)
717
718 self.pixelsX = None
719 self.pixelsY = None
720 elif angle == 270:
721 if imageCache is not None:
722 Exiftran.rotate270(self.fn)
723
724 newPixbuffer = self.scaledPixbuffer.rotate_simple(gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE)
725 self.pixelsX = y
726 self.pixelsY = x
727 self.metadata["Resolution"] = "%i x % i" % (y, x)
728 else:
729 Exiftran.rotate270(self.fn)
730
731 self.pixelsX = None
732 self.pixelsY = None
733 elif angle == 180:
734 if imageCache is not None:
735 Exiftran.rotate180(self.fn)
736
737 newPixbuffer = self.scaledPixbuffer.rotate_simple(gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN)
738 else:
739 Exiftran.rotate180(self.fn)
740
741 self.pixelsX = None
742 self.pixelsY = None
743 else:
744 print "Erreur ! il n'est pas possible de faire une rotation de ce type sans perte de donnée."
745 if imageCache is not None:
746 self.scaledPixbuffer = newPixbuffer
747 if config.DEBUG:
748 print("After rotation %i, x=%i, y=%i, scaledX=%i, scaledY=%i" % (angle, self.pixelsX, self.pixelsY, self.scaledPixbuffer.get_width(), self.scaledPixbuffer.get_height()))
749
750
759
760
762 """Send the file to the trash folder"""
763 self.RemoveFromCache()
764 Trashdir = os.path.join(config.DefaultRepository, config.TrashDirectory)
765 td = os.path.dirname(os.path.join(Trashdir, self.filename))
766 if not os.path.isdir(td): makedir(td)
767 shutil.move(self.fn, os.path.join(Trashdir, self.filename))
768
769
771 """return exif data + title from the photo"""
772 clef = {'Exif.Image.Make':'Marque',
773 'Exif.Image.Model':'Modele',
774 'Exif.Photo.DateTimeOriginal':'Heure',
775 'Exif.Photo.ExposureTime':'Vitesse',
776 'Exif.Photo.FNumber':'Ouverture',
777
778 'Exif.Photo.ExposureBiasValue':'Bias',
779 'Exif.Photo.Flash':'Flash',
780 'Exif.Photo.FocalLength':'Focale',
781 'Exif.Photo.ISOSpeedRatings':'Iso' ,
782
783 }
784
785 if self.metadata is None:
786 self.metadata = {}
787 self.metadata["Taille"] = "%.2f %s" % SmartSize(os.path.getsize(self.fn))
788 self.exif = pyexiv2.Image(self.fn)
789 self.exif.readMetadata()
790 self.metadata["Titre"] = self.exif.getComment()
791 if self.pixelsX and self.pixelsY:
792 self.metadata["Resolution"] = "%s x %s " % (self.pixelsX, self.pixelsY)
793 else:
794 try:
795 self.pixelsX = self.exif["Exif.Photo.PixelXDimension"]
796 self.pixelsY = self.exif["Exif.Photo.PixelYDimension"]
797 except (IndexError, KeyError):
798 self.taille()
799 self.metadata["Resolution"] = "%s x %s " % (self.pixelsX, self.pixelsY)
800 if "Exif.Image.Orientation" in self.exif.exifKeys():
801 self.orientation = self.exif["Exif.Image.Orientation"]
802 for key in clef:
803 try:
804 self.metadata[clef[key]] = self.exif.interpretedExifValue(key).decode(config.Coding).strip()
805 except:
806 self.metadata[clef[key]] = u""
807 return self.metadata.copy()
808
809
811 """return true if the image is entitled"""
812 if self.metadata == None:
813 self.readExif()
814 if self.metadata["Titre"]:
815 return True
816 else:
817 return False
818
819
820 - def show(self, Xsize=600, Ysize=600):
821 """return a pixbuf to shows the image in a Gtk window"""
822 scaled_buf = None
823 if Xsize > config.ImageWidth :
824 config.ImageWidth = Xsize
825 if Ysize > config.ImageHeight:
826 config.ImageHeight = Ysize
827 self.taille()
828
829
830 Rbig = min(float(config.ImageWidth) / self.pixelsX, float(config.ImageHeight) / self.pixelsY)
831 if Rbig < 1:
832 nxBig = int(round(Rbig * self.pixelsX))
833 nyBig = int(round(Rbig * self.pixelsY))
834 else:
835 nxBig = self.pixelsX
836 nyBig = self.pixelsY
837
838 R = min(float(Xsize) / self.pixelsX, float(Ysize) / self.pixelsY)
839 if R < 1:
840 nx = int(round(R * self.pixelsX))
841 ny = int(round(R * self.pixelsY))
842 else:
843 nx = self.pixelsX
844 ny = self.pixelsY
845
846 if self.scaledPixbuffer is None:
847 pixbuf = gtk.gdk.pixbuf_new_from_file(self.fn)
848
849 if Rbig < 1:
850 self.scaledPixbuffer = pixbuf.scale_simple(nxBig, nyBig, gtkInterpolation[config.Interpolation])
851 else :
852 self.scaledPixbuffer = pixbuf
853 if config.DEBUG:
854 print("Sucessfully cached %s, size (%i,%i)" % (self.filename, nxBig, nyBig))
855 if (self.scaledPixbuffer.get_width() == nx) and (self.scaledPixbuffer.get_height() == ny):
856 scaled_buf = self.scaledPixbuffer
857 if config.DEBUG:
858 print("Sucessfully fetched %s from cache, directly with the right size." % (self.filename))
859 else:
860 if config.DEBUG:
861 print("%s pixmap in buffer but not with the right shape: nx=%i,\tny=%i,\tw=%i,h=%i" % (self.filename, nx, ny, self.scaledPixbuffer.get_width(), self.scaledPixbuffer.get_height()))
862 scaled_buf = self.scaledPixbuffer.scale_simple(nx, ny, gtkInterpolation[config.Interpolation])
863 return scaled_buf
864
865
866 - def name(self, titre):
867 """write the title of the photo inside the description field, in the JPEG header"""
868 if os.name == 'nt' and self.pil != None:
869 self.pil = None
870 self.metadata["Titre"] = titre
871 self.exif.setComment(titre)
872 self.exif.writeMetadata()
873
874
876 """
877 rename the current instance of photo:
878 -Move the file
879 -update the cache
880 -change the name and other attributes of the instance
881 -change the exif metadata.
882 """
883 oldname = self.filename
884 newfn = os.path.join(config.DefaultRepository, newname)
885 os.rename(self.fn, newfn)
886 self.filename = newname
887 self.fn = newfn
888 self.exif = newfn
889 if self.exif is not None:
890 self.exif = pyexiv2.Image(self.fn)
891 self.exif.readMetadata()
892 if (imageCache is not None) and oldname in imageCache:
893 imageCache.rename(oldname, newname)
894
895
897 """
898 Save the original name of the file into the Exif.Photo.UserComment tag.
899 This tag is usually not used, people prefer the JPEG tag for entiteling images.
900
901 @param originalName: name of the file before it was processed by selector
902 @type originalName: python string
903 """
904 if self.metadata == None:
905 self.readExif()
906 self.exif["Exif.Photo.UserComment"] = originalName
907 self.exif.writeMetadata()
908
909
911 """does autorotate the image according to the EXIF tag"""
912 if os.name == 'nt' and self.pil is not None:
913 del self.pil
914
915 self.readExif()
916 if self.orientation != 1:
917 Exiftran.autorotate(self.fn)
918
919 if self.orientation > 4:
920 self.pixelsX = self.exif["Exif.Photo.PixelYDimension"]
921 self.pixelsY = self.exif["Exif.Photo.PixelXDimension"]
922 self.metadata["Resolution"] = "%s x %s " % (self.pixelsX, self.pixelsY)
923 self.orientation = 1
924
925
927 """Ceci est un filtre de debouchage de photographies, aussi appelé masque de contraste, il permet de rattrapper une photo trop contrasté, un contre jour, ...
928 Écrit par Jérôme Kieffer, avec l'aide de la liste python@aful, en particulier A. Fayolles et F. Mantegazza
929 avril 2006
930 necessite numpy et PIL."""
931 try:
932 import numpy
933 import scipy
934 import scipy.signal as signal
935 except:
936 raise ImportError("This filter needs the numpy library available on https://sourceforge.net/projects/numpy/files/")
937
938 if photo.GaussianKernel is None:
939 size = 15
940 x, y = numpy.mgrid[-size:size + 1, -size:size + 1]
941 g = numpy.exp(-(x ** 2 / float(size) + y ** 2 / float(size)))
942 photo.GaussianKernel = g / g.sum()
943
944 self.LoadPIL()
945 x, y = self.pil.size
946 ImageFile.MAXBLOCK = x * y
947 img_array = numpy.fromstring(self.pil.tostring(), dtype="UInt8").astype("UInt16")
948 img_array.shape = (y, x, 3)
949 red, green, blue = img_array[:, :, 0], img_array[:, :, 1], img_array[:, :, 2]
950 desat_array = (numpy.minimum(numpy.minimum(red, green), blue) + numpy.maximum(numpy.maximum(red, green), blue)) / 2
951 inv_desat = 255 - desat_array
952 blured_inv_desat = signal.convolve(inv_desat, photo.GaussianKernel, mode='valid')
953
954 k = Image.fromarray(blured_inv_desat, "L").convert("RGB")
955 S = ImageChops.screen(self.pil, k)
956 M = ImageChops.multiply(self.pil, k)
957 F = ImageChops.add(ImageChops.multiply(self.pil, S), ImageChops.multiply(ImageChops.invert(self.pil), M))
958 F.save(os.path.join(config.DefaultRepository, outfile), quality=90, progressive=True, Optimize=True)
959 try:
960 os.chmod(os.path.join(config.DefaultRepository, outfile), config.DefaultFileMode)
961 except:
962 print("Warning: unable to chmod %s" % outfile)
963
964
965
966
967
968
971 """
972 this filter allows add a signature to an image
973 """
974 self.img = None
975 self.sig = Image.open(filename)
976 self.sig.convert("RGB")
977 (self.xs, self.ys) = self.sig.size
978 self.bigsig = self.sig
979
980 if ImageStat.Stat(self.sig)._getmean() > 127:
981 self.sig = ImageChops.invert(self.sig)
982
983 self.orientation = -1
984 (self.x, self.y) = (self.xs, self.ys)
985
986 - def mask(self, orientation=5):
987 """
988 x and y are the size of the initial image
989 the orientation correspond to the position on a clock :
990 0 for the center
991 1 or 2 upper right
992 3 centered in heith right side ...."""
993 if orientation == self.orientation and (self.x, self.y) == self.bigsig.size:
994
995 return
996 self.orientation = orientation
997 self.bigsig = Image.new("RGB", (self.x, self.y), (0, 0, 0))
998 if self.x < self.xs or self.y < self.ys :
999
1000 return
1001 if self.orientation == 0:
1002 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))
1003 elif self.orientation in [1, 2]:
1004 self.bigsig.paste(self.sig, (self.x - self.xs, 0, self.x, self.ys))
1005 elif self.orientation == 3:
1006 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))
1007 elif self.orientation in [ 5, 4]:
1008 self.bigsig.paste(self.sig, (self.x - self.xs, self.y - self.ys, self.x, self.y))
1009 elif self.orientation == 6:
1010 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))
1011 elif self.orientation in [7, 8]:
1012 self.bigsig.paste(self.sig, (0, self.y - self.ys, self.xs, self.y))
1013 elif self.orientation == 9:
1014 self.bigsig.paste(self.sig, (0, self.y / 2 - self.ys / 2, self.xs, self.y / 2 - self.ys / 2 + self.ys))
1015 elif self.orientation in [10, 11]:
1016 self.bigsig.paste(self.sig, (0, 0, self.xs, self.ys))
1017 elif self.orientation == 12:
1018 self.bigsig.paste(self.sig, (self.x / 2 - self.xs / 2, 0, self.x / 2 - self.xs / 2 + self.xs, self.ys))
1019 return
1020
1021 - def substract(self, inimage, orientation=5):
1022 """apply a substraction mask on the image"""
1023 self.img = inimage
1024 self.x, self.y = self.img.size
1025 ImageFile.MAXBLOCK = self.x * self.y
1026 self.mask(orientation)
1027 k = ImageChops.difference(self.img, self.bigsig)
1028 return k
1029
1031 """ class for handling raw images
1032 - extract thumbnails
1033 - copy them in the repository
1034 """
1036 """
1037 Contructor of the class
1038
1039 @param strRawFile: path to the RawImage
1040 @type strRawFile: string
1041 """
1042 self.strRawFile = strRawFile
1043 self.exif = None
1044 self.strJepgFile = None
1045
1046 print("Importing [Raw|Jpeg] image %s" % strRawFile)
1047
1049
1050 if self.exif is None:
1051 self.exif = pyexiv2.Image(self.strRawFile)
1052 self.exif.readMetadata()
1053 if self.strJepgFile is None:
1054 self.strJepgFile = unicode_to_ascii("%s-%s.jpg" % (
1055 self.exif.interpretedExifValue("Exif.Photo.DateTimeOriginal").replace(" ", os.sep).replace(":", "-", 2).replace(":", "h", 1).replace(":", "m", 1),
1056 self.exif.interpretedExifValue("Exif.Image.Model").strip().split(",")[-1].replace("/", "").replace(" ", "_")
1057 ))
1058 while os.path.isfile(os.path.join(config.DefaultRepository, self.strJepgFile)):
1059 number = ""
1060 idx = None
1061 listChar = list(self.strJepgFile[:-4])
1062 listChar.reverse()
1063 for val in listChar:
1064 if val.isdigit():
1065 number = val + number
1066 elif val == "-":
1067 idx = int(number)
1068 break
1069 else:
1070 break
1071 if idx is None:
1072 self.strJepgFile = self.strJepgFile[:-4] + "-1.jpg"
1073 else:
1074 self.strJepgFile = self.strJepgFile[:-5 - len(number)] + "-%i.jpg" % (idx + 1)
1075 dirname = os.path.dirname(os.path.join(config.DefaultRepository, self.strJepgFile))
1076 if not os.path.isdir(dirname):
1077 makedir(dirname)
1078
1079 return self.strJepgFile
1080
1082 """
1083 extract the raw image to its right place
1084 """
1085 extension = os.path.splitext(self.strRawFile)[1].lower()
1086 strJpegFullPath = os.path.join(config.DefaultRepository, self.getJpegPath())
1087 if extension in config.RawExtensions:
1088 data = os.popen("%s %s" % (config.Dcraw, self.strRawFile)).readlines()
1089 img = Image.fromstring("RGB", tuple([int(i) for i in data[1].split()]), "".join(tuple(data[3:])))
1090 img.save(strJpegFullPath, format='JPEG')
1091
1092 exifJpeg = pyexiv2.Image(strJpegFullPath)
1093 exifJpeg.readMetadata()
1094 exifJpeg['Exif.Image.Orientation'] = 1
1095 exifJpeg["Exif.Photo.UserComment"] = self.strRawFile
1096 for metadata in [ 'Exif.Image.Make', 'Exif.Image.Model', 'Exif.Photo.DateTimeOriginal', 'Exif.Photo.ExposureTime', 'Exif.Photo.FNumber', 'Exif.Photo.ExposureBiasValue', 'Exif.Photo.Flash', 'Exif.Photo.FocalLength', 'Exif.Photo.ISOSpeedRatings']:
1097 try:
1098 exifJpeg[metadata] = self.exif[metadata]
1099 except:
1100 print("error in copying metadata %s in file %s, value: %s" % (metadata, self.strRawFile, self.exif[metadata]))
1101
1102 exifJpeg.writeMetadata()
1103
1104 else:
1105 shutil.copy(self.strRawFile, strJpegFullPath)
1106 Exiftran.autorotate(strJpegFullPath)
1107
1108 os.chmod(strJpegFullPath, config.DefaultFileMode)
1109
1110
1111
1112
1113
1114
1115
1117 """creates the tree structure for the file"""
1118 dire = os.path.dirname(filen)
1119 if os.path.isdir(dire):
1120 mkdir(filen)
1121 else:
1122 makedir(dire)
1123 mkdir(filen)
1124
1126 """create an empty directory with the given rights"""
1127
1128 os.mkdir(filename)
1129 try:
1130 os.chmod(filename, config.DefaultDirMode)
1131 except OSError:
1132 print("Warning: unable to chmod %s" % filename)
1133
1134 -def findFiles(strRootDir, lstExtentions=config.Extensions, bFromRoot=False):
1135 """
1136 Equivalent to:
1137 files=os.system('find "%s" -iname "*.%s"'%(RootDir,suffix)).readlines()
1138
1139 @param strRootDir: path of the root of the search
1140 @type strRootDir: string
1141 @param lstExtentions: list of string representing interesting extensions
1142 @param bFromRoot: start the return path from / instead of the strRootDir
1143 @return: the list of the files with the given suffix in the given dir
1144 @rtype: list of strings
1145 """
1146 listFiles = []
1147 if strRootDir.endswith("os.sep"):
1148 lenRoot = len(strRootDir)
1149 else:
1150 lenRoot = len(strRootDir) + 1
1151 for root, dirs, files in os.walk(strRootDir):
1152 for oneFile in files:
1153 if os.path.splitext(oneFile)[1].lower() in lstExtentions:
1154 fullPath = os.path.join(root, oneFile)
1155 if bFromRoot:
1156 listFiles.append(fullPath)
1157 else:
1158 assert len(fullPath) > lenRoot
1159 listFiles.append(fullPath[lenRoot:])
1160 return listFiles
1161
1162
1163
1164
1165
1167 """common processing for one image : create a subfolder "scaled" and "thumb" : """
1168
1169 rootdir = os.path.dirname(filename)
1170 scaledir = os.path.join(rootdir, config.ScaledImages["Suffix"])
1171 thumbdir = os.path.join(rootdir, config.Thumbnails["Suffix"])
1172 if not os.path.isdir(scaledir) : mkdir(scaledir)
1173 if not os.path.isdir(thumbdir) : mkdir(thumbdir)
1174 Img = photo(filename)
1175 Param = config.ScaledImages.copy()
1176 Param.pop("Suffix")
1177 Param["strThumbFile"] = os.path.join(scaledir, os.path.basename(filename))[:-4] + "--%s.jpg" % config.ScaledImages["Suffix"]
1178 Img.SaveThumb(**Param)
1179 Param = config.Thumbnails.copy()
1180 Param.pop("Suffix")
1181 Param["strThumbFile"] = os.path.join(thumbdir, os.path.basename(filename))[:-4] + "--%s.jpg" % config.Thumbnails["Suffix"]
1182 Img.SaveThumb(**Param)
1183 if filigrane:
1184 filigrane.substract(Img.f).save(filename, quality=config.FiligraneQuality, optimize=config.FiligraneOptimize, progressive=config.FiligraneOptimize)
1185 try:
1186 os.chmod(filename, config.DefaultFileMode)
1187 except OSError:
1188 print("Warning: unable to chmod %s" % filename)
1189
1191 """
1192 This takes a UNICODE string and replaces unicode characters with
1193 something equivalent in 7-bit ASCII. It returns a plain ASCII string.
1194 This function makes a best effort to convert unicode characters into
1195 ASCII equivalents. It does not just strip out the Latin-1 characters.
1196 All characters in the standard 7-bit ASCII range are preserved.
1197 In the 8th bit range all the Latin-1 accented letters are converted
1198 to unaccented equivalents. Most symbol characters are converted to
1199 something meaningful. Anything not converted is deleted.
1200 """
1201 xlate = {0xc0:'A', 0xc1:'A', 0xc2:'A', 0xc3:'A', 0xc4:'A', 0xc5:'A',
1202 0xc6:'Ae', 0xc7:'C',
1203 0xc8:'E', 0xc9:'E', 0xca:'E', 0xcb:'E',
1204 0xcc:'I', 0xcd:'I', 0xce:'I', 0xcf:'I',
1205 0xd0:'Th', 0xd1:'N',
1206 0xd2:'O', 0xd3:'O', 0xd4:'O', 0xd5:'O', 0xd6:'O', 0xd8:'O',
1207 0xd9:'U', 0xda:'U', 0xdb:'U', 0xdc:'U',
1208 0xdd:'Y', 0xde:'th', 0xdf:'ss',
1209 0xe0:'a', 0xe1:'a', 0xe2:'a', 0xe3:'a', 0xe4:'a', 0xe5:'a',
1210 0xe6:'ae', 0xe7:'c',
1211 0xe8:'e', 0xe9:'e', 0xea:'e', 0xeb:'e',
1212 0xec:'i', 0xed:'i', 0xee:'i', 0xef:'i',
1213 0xf0:'th', 0xf1:'n',
1214 0xf2:'o', 0xf3:'o', 0xf4:'o', 0xf5:'o', 0xf6:'o', 0xf8:'o',
1215 0xf9:'u', 0xfa:'u', 0xfb:'u', 0xfc:'u',
1216 0xfd:'y', 0xfe:'th', 0xff:'y',
1217 0xa1:'!', 0xa2:'{cent}', 0xa3:'{pound}', 0xa4:'{currency}',
1218 0xa5:'{yen}', 0xa6:'|', 0xa7:'{section}', 0xa8:'{umlaut}',
1219 0xa9:'{C}', 0xaa:'{^a}', 0xab:'<<', 0xac:'{not}',
1220 0xad:'-', 0xae:'{R}', 0xaf:'_', 0xb0:'{degrees}',
1221 0xb1:'{+/-}', 0xb2:'{^2}', 0xb3:'{^3}', 0xb4:"'",
1222 0xb5:'{micro}', 0xb6:'{paragraph}', 0xb7:'*', 0xb8:'{cedilla}',
1223 0xb9:'{^1}', 0xba:'{^o}', 0xbb:'>>',
1224 0xbc:'{1/4}', 0xbd:'{1/2}', 0xbe:'{3/4}', 0xbf:'?',
1225 0xd7:'*', 0xf7:'/'
1226 }
1227
1228 r = []
1229 for i in unicrap:
1230 if xlate.has_key(ord(i)):
1231 r.append(xlate[ord(i)])
1232 elif ord(i) >= 0x80:
1233 pass
1234 else:
1235 r.append(str(i))
1236 return "".join(r)
1237
1239 """print the size of files in a pretty way"""
1240 unit = "o"
1241 fsize = float(size)
1242 if len(str(size)) > 3:
1243 size /= 1024
1244 fsize /= 1024.0
1245 unit = "ko"
1246 if len(str(size)) > 3:
1247 size = size / 1024
1248 fsize /= 1024.0
1249 unit = "Mo"
1250 if len(str(size)) > 3:
1251 size = size / 1024
1252 fsize /= 1024.0
1253 unit = "Go"
1254 return fsize, unit
1255
1256
1258 """
1259 Delete everything reachable from the directory named in "top",
1260 assuming there are no symbolic links.
1261 CAUTION: This is dangerous! For example, if top == '/', it
1262 could delete all your disk files.
1263 @param strDirname: top directory to delete
1264 @type strDirname: string
1265 """
1266 for root, dirs, files in os.walk(strDirname, topdown=False):
1267 for name in files:
1268 os.remove(os.path.join(root, name))
1269 for name in dirs:
1270 os.rmdir(os.path.join(root, name))
1271 os.rmdir(strDirname)
1272
1273
1274
1275
1276 if __name__ == "__main__":
1277
1278
1279 config.DefaultRepository = os.path.abspath(sys.argv[1])
1280 print config.DefaultRepository
1281 RangeTout(sys.argv[1])
1282