2011-05-06 00:37:15 +02:00
from CachePath import *
2011-05-05 13:04:40 +02:00
from datetime import datetime
import json
2011-05-07 13:42:34 +02:00
import os
2011-05-05 13:04:40 +02:00
import os . path
from PIL import Image
from PIL . ExifTags import TAGS
2014-03-16 15:17:13 +01:00
from multiprocessing import Pool
2011-05-06 08:43:54 +02:00
import gc
2011-05-05 13:04:40 +02:00
2014-03-16 15:17:13 +01:00
def make_thumbs ( self , original_path , thumb_path , size ) :
# The pool methods use a queue.Queue to pass tasks to the worker processes.
# Everything that goes through the queue.Queue must be pickable, and since
# self._thumbnail is not defined at the top level, it's not pickable.
# This is why we have this "dummy" function, so that it's pickable.
self . _thumbnail ( original_path , thumb_path , size [ 0 ] , size [ 1 ] )
2011-05-05 13:04:40 +02:00
class Album ( object ) :
def __init__ ( self , path ) :
self . _path = trim_base ( path )
self . _photos = list ( )
self . _albums = list ( )
self . _photos_sorted = True
self . _albums_sorted = True
@property
2011-05-06 00:37:15 +02:00
def photos ( self ) :
return self . _photos
@property
def albums ( self ) :
return self . _albums
@property
2011-05-05 13:04:40 +02:00
def path ( self ) :
return self . _path
def __str__ ( self ) :
return self . path
@property
def cache_path ( self ) :
return json_cache ( self . path )
@property
def date ( self ) :
self . _sort ( )
if len ( self . _photos ) == 0 and len ( self . _albums ) == 0 :
2011-05-05 14:42:04 +02:00
return datetime ( 1900 , 1 , 1 )
2011-05-05 13:04:40 +02:00
elif len ( self . _photos ) == 0 :
return self . _albums [ - 1 ] . date
elif len ( self . _albums ) == 0 :
return self . _photos [ - 1 ] . date
return max ( self . _photos [ - 1 ] . date , self . _albums [ - 1 ] . date )
def __cmp__ ( self , other ) :
return cmp ( self . date , other . date )
def add_photo ( self , photo ) :
self . _photos . append ( photo )
self . _photos_sorted = False
def add_album ( self , album ) :
self . _albums . append ( album )
2011-05-06 14:38:15 +02:00
self . _albums_sorted = False
2011-05-05 13:04:40 +02:00
def _sort ( self ) :
if not self . _photos_sorted :
self . _photos . sort ( )
self . _photos_sorted = True
if not self . _albums_sorted :
self . _albums . sort ( )
self . _albums_sorted = True
2011-05-07 04:48:09 +02:00
@property
def empty ( self ) :
if len ( self . _photos ) != 0 :
return False
if len ( self . _albums ) == 0 :
return True
for album in self . _albums :
if not album . empty :
return False
return True
2011-05-05 13:04:40 +02:00
def cache ( self , base_dir ) :
self . _sort ( )
fp = open ( os . path . join ( base_dir , self . cache_path ) , ' w ' )
json . dump ( self , fp , cls = PhotoAlbumEncoder )
fp . close ( )
@staticmethod
def from_cache ( path ) :
fp = open ( path , " r " )
dictionary = json . load ( fp )
fp . close ( )
return Album . from_dict ( dictionary )
@staticmethod
def from_dict ( dictionary , cripple = True ) :
album = Album ( dictionary [ " path " ] )
for photo in dictionary [ " photos " ] :
2011-05-05 14:42:04 +02:00
album . add_photo ( Photo . from_dict ( photo , untrim_base ( album . path ) ) )
2011-05-05 13:04:40 +02:00
if not cripple :
for subalbum in dictionary [ " albums " ] :
album . add_album ( Album . from_dict ( subalbum ) , cripple )
album . _sort ( )
return album
def to_dict ( self , cripple = True ) :
self . _sort ( )
2011-05-07 04:48:09 +02:00
subalbums = [ ]
2011-05-05 13:04:40 +02:00
if cripple :
2011-05-07 04:48:09 +02:00
for sub in self . _albums :
if not sub . empty :
subalbums . append ( { " path " : trim_base_custom ( sub . path , self . _path ) , " date " : sub . date } )
2011-05-05 13:04:40 +02:00
else :
2011-05-07 04:48:09 +02:00
for sub in self . _albums :
if not sub . empty :
subalbums . append ( sub )
2011-05-05 13:04:40 +02:00
return { " path " : self . path , " date " : self . date , " albums " : subalbums , " photos " : self . _photos }
2011-05-05 14:42:04 +02:00
def photo_from_path ( self , path ) :
for photo in self . _photos :
if trim_base ( path ) == photo . _path :
return photo
return None
2011-05-05 13:04:40 +02:00
class Photo ( object ) :
2014-03-16 15:03:54 +01:00
thumb_sizes = [ ( 75 , True ) , ( 150 , True ) , ( 640 , False ) , ( 1024 , False ) , ( 1600 , False ) ]
2011-05-05 14:42:04 +02:00
def __init__ ( self , path , thumb_path = None , attributes = None ) :
2011-05-05 13:04:40 +02:00
self . _path = trim_base ( path )
self . is_valid = True
2011-05-06 00:37:15 +02:00
try :
mtime = file_mtime ( path )
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-06 00:37:15 +02:00
except :
self . is_valid = False
return
2011-05-07 08:56:54 +02:00
if attributes is not None and attributes [ " dateTimeFile " ] > = mtime :
2011-05-05 13:04:40 +02:00
self . _attributes = attributes
return
2011-05-05 13:21:12 +02:00
self . _attributes = { }
2011-05-07 08:56:54 +02:00
self . _attributes [ " dateTimeFile " ] = mtime
2014-03-16 15:17:13 +01:00
2011-05-05 13:04:40 +02:00
try :
2011-05-05 14:42:04 +02:00
image = Image . open ( path )
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-05 13:04:40 +02:00
except :
self . is_valid = False
return
2014-03-16 15:17:13 +01:00
self . _metadata ( path )
self . _thumbnails ( path , thumb_path )
def _metadata ( self , path ) :
2011-05-06 13:55:47 +02:00
self . _attributes [ " size " ] = image . size
2011-05-07 08:56:54 +02:00
self . _orientation = 1
2011-05-05 13:04:40 +02:00
try :
2011-05-05 14:42:04 +02:00
info = image . _getexif ( )
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-05 13:04:40 +02:00
except :
2011-05-05 14:42:04 +02:00
return
2011-05-06 00:37:15 +02:00
if not info :
return
2011-05-07 08:56:54 +02:00
exif = { }
2011-05-05 14:42:04 +02:00
for tag , value in info . items ( ) :
decoded = TAGS . get ( tag , tag )
2011-05-07 08:56:54 +02:00
if isinstance ( value , str ) :
2012-03-29 15:42:53 +02:00
value = value . strip ( ) . partition ( " \x00 " ) [ 0 ]
2011-05-07 09:12:20 +02:00
if isinstance ( decoded , str ) and decoded . startswith ( " DateTime " ) :
2011-05-07 08:56:54 +02:00
try :
value = datetime . strptime ( value , ' % Y: % m: %d % H: % M: % S ' )
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-07 08:56:54 +02:00
except :
2011-05-07 09:00:06 +02:00
continue
2011-05-07 08:56:54 +02:00
exif [ decoded ] = value
if " Orientation " in exif :
self . _orientation = exif [ " Orientation " ] ;
if self . _orientation in range ( 5 , 9 ) :
self . _attributes [ " size " ] = ( self . _attributes [ " size " ] [ 1 ] , self . _attributes [ " size " ] [ 0 ] )
2011-05-10 00:08:07 +02:00
if self . _orientation - 1 < len ( self . _metadata . orientation_list ) :
self . _attributes [ " orientation " ] = self . _metadata . orientation_list [ self . _orientation - 1 ]
2011-05-07 08:56:54 +02:00
if " Make " in exif :
self . _attributes [ " make " ] = exif [ " Make " ]
if " Model " in exif :
self . _attributes [ " model " ] = exif [ " Model " ]
if " ApertureValue " in exif :
self . _attributes [ " aperture " ] = exif [ " ApertureValue " ]
2011-05-07 09:42:44 +02:00
elif " FNumber " in exif :
self . _attributes [ " aperture " ] = exif [ " FNumber " ]
2011-05-07 08:56:54 +02:00
if " FocalLength " in exif :
self . _attributes [ " focalLength " ] = exif [ " FocalLength " ]
if " ISOSpeedRatings " in exif :
self . _attributes [ " iso " ] = exif [ " ISOSpeedRatings " ]
if " ISO " in exif :
self . _attributes [ " iso " ] = exif [ " ISO " ]
if " PhotographicSensitivity " in exif :
self . _attributes [ " iso " ] = exif [ " PhotographicSensitivity " ]
if " ExposureTime " in exif :
self . _attributes [ " exposureTime " ] = exif [ " ExposureTime " ]
2011-05-10 00:08:07 +02:00
if " Flash " in exif and exif [ " Flash " ] in self . _metadata . flash_dictionary :
2011-05-07 09:42:44 +02:00
try :
self . _attributes [ " flash " ] = self . _metadata . flash_dictionary [ exif [ " Flash " ] ]
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-07 09:42:44 +02:00
except :
pass
2011-06-27 23:12:59 +02:00
if " LightSource " in exif and exif [ " LightSource " ] in self . _metadata . light_source_dictionary :
try :
self . _attributes [ " lightSource " ] = self . _metadata . light_source_dictionary [ exif [ " LightSource " ] ]
except KeyboardInterrupt :
raise
except :
pass
2011-05-10 00:08:07 +02:00
if " ExposureProgram " in exif and exif [ " ExposureProgram " ] < len ( self . _metadata . exposure_list ) :
2011-05-07 09:42:44 +02:00
self . _attributes [ " exposureProgram " ] = self . _metadata . exposure_list [ exif [ " ExposureProgram " ] ]
2011-05-07 08:56:54 +02:00
if " SpectralSensitivity " in exif :
self . _attributes [ " spectralSensitivity " ] = exif [ " SpectralSensitivity " ]
2011-05-10 00:08:07 +02:00
if " MeteringMode " in exif and exif [ " MeteringMode " ] < len ( self . _metadata . metering_list ) :
self . _attributes [ " meteringMode " ] = self . _metadata . metering_list [ exif [ " MeteringMode " ] ]
2011-06-27 23:12:59 +02:00
if " SensingMethod " in exif and exif [ " SensingMethod " ] < len ( self . _metadata . sensing_method_list ) :
self . _attributes [ " sensingMethod " ] = self . _metadata . sensing_method_list [ exif [ " SensingMethod " ] ]
if " SceneCaptureType " in exif and exif [ " SceneCaptureType " ] < len ( self . _metadata . scene_capture_type_list ) :
self . _attributes [ " sceneCaptureType " ] = self . _metadata . scene_capture_type_list [ exif [ " SceneCaptureType " ] ]
if " SubjectDistanceRange " in exif and exif [ " SubjectDistanceRange " ] < len ( self . _metadata . subject_distance_range_list ) :
self . _attributes [ " subjectDistanceRange " ] = self . _metadata . subject_distance_range_list [ exif [ " SubjectDistanceRange " ] ]
2011-05-07 08:56:54 +02:00
if " ExposureCompensation " in exif :
self . _attributes [ " exposureCompensation " ] = exif [ " ExposureCompensation " ]
if " ExposureBiasValue " in exif :
self . _attributes [ " exposureCompensation " ] = exif [ " ExposureBiasValue " ]
if " DateTimeOriginal " in exif :
self . _attributes [ " dateTimeOriginal " ] = exif [ " DateTimeOriginal " ]
if " DateTime " in exif :
self . _attributes [ " dateTime " ] = exif [ " DateTime " ]
2011-05-07 09:42:44 +02:00
_metadata . flash_dictionary = { 0x0 : " No Flash " , 0x1 : " Fired " , 0x5 : " Fired, Return not detected " , 0x7 : " Fired, Return detected " , 0x8 : " On, Did not fire " , 0x9 : " On, Fired " , 0xd : " On, Return not detected " , 0xf : " On, Return detected " , 0x10 : " Off, Did not fire " , 0x14 : " Off, Did not fire, Return not detected " , 0x18 : " Auto, Did not fire " , 0x19 : " Auto, Fired " , 0x1d : " Auto, Fired, Return not detected " , 0x1f : " Auto, Fired, Return detected " , 0x20 : " No flash function " , 0x30 : " Off, No flash function " , 0x41 : " Fired, Red-eye reduction " , 0x45 : " Fired, Red-eye reduction, Return not detected " , 0x47 : " Fired, Red-eye reduction, Return detected " , 0x49 : " On, Red-eye reduction " , 0x4d : " On, Red-eye reduction, Return not detected " , 0x4f : " On, Red-eye reduction, Return detected " , 0x50 : " Off, Red-eye reduction " , 0x58 : " Auto, Did not fire, Red-eye reduction " , 0x59 : " Auto, Fired, Red-eye reduction " , 0x5d : " Auto, Fired, Red-eye reduction, Return not detected " , 0x5f : " Auto, Fired, Red-eye reduction, Return detected " }
2011-06-27 23:12:59 +02:00
_metadata . light_source_dictionary = { 0 : " Unknown " , 1 : " Daylight " , 2 : " Fluorescent " , 3 : " Tungsten (incandescent light) " , 4 : " Flash " , 9 : " Fine weather " , 10 : " Cloudy weather " , 11 : " Shade " , 12 : " Daylight fluorescent (D 5700 - 7100K) " , 13 : " Day white fluorescent (N 4600 - 5400K) " , 14 : " Cool white fluorescent (W 3900 - 4500K) " , 15 : " White fluorescent (WW 3200 - 3700K) " , 17 : " Standard light A " , 18 : " Standard light B " , 19 : " Standard light C " , 20 : " D55 " , 21 : " D65 " , 22 : " D75 " , 23 : " D50 " , 24 : " ISO studio tungsten " }
2011-05-07 09:42:44 +02:00
_metadata . metering_list = [ " Unknown " , " Average " , " Center-weighted average " , " Spot " , " Multi-spot " , " Multi-segment " , " Partial " ]
_metadata . exposure_list = [ " Not Defined " , " Manual " , " Program AE " , " Aperture-priority AE " , " Shutter speed priority AE " , " Creative (Slow speed) " , " Action (High speed) " , " Portrait " , " Landscape " , " Bulb " ]
_metadata . orientation_list = [ " Horizontal (normal) " , " Mirror horizontal " , " Rotate 180 " , " Mirror vertical " , " Mirror horizontal and rotate 270 CW " , " Rotate 90 CW " , " Mirror horizontal and rotate 90 CW " , " Rotate 270 CW " ]
2011-06-27 23:12:59 +02:00
_metadata . sensing_method_list = [ " Not defined " , " One-chip color area sensor " , " Two-chip color area sensor " , " Three-chip color area sensor " , " Color sequential area sensor " , " Trilinear sensor " , " Color sequential linear sensor " ]
_metadata . scene_capture_type_list = [ " Standard " , " Landscape " , " Portrait " , " Night scene " ]
_metadata . subject_distance_range_list = [ " Unknown " , " Macro " , " Close view " , " Distant view " ]
2011-05-06 13:55:47 +02:00
2014-03-16 15:19:52 +01:00
def _thumbnail ( self , original_path , thumb_path , size , square = False ) :
try :
2014-03-16 15:17:13 +01:00
image = Image . open ( original_path )
except KeyboardInterrupt :
raise
except :
self . is_valid = False
return
mirror = image
if self . _orientation == 2 :
# Vertical Mirror
mirror = image . transpose ( Image . FLIP_LEFT_RIGHT )
elif self . _orientation == 3 :
# Rotation 180
mirror = image . transpose ( Image . ROTATE_180 )
elif self . _orientation == 4 :
# Horizontal Mirror
mirror = image . transpose ( Image . FLIP_TOP_BOTTOM )
elif self . _orientation == 5 :
# Horizontal Mirror + Rotation 270
mirror = image . transpose ( Image . FLIP_TOP_BOTTOM ) . transpose ( Image . ROTATE_270 )
elif self . _orientation == 6 :
# Rotation 270
mirror = image . transpose ( Image . ROTATE_270 )
elif self . _orientation == 7 :
# Vertical Mirror + Rotation 270
mirror = image . transpose ( Image . FLIP_LEFT_RIGHT ) . transpose ( Image . ROTATE_270 )
elif self . _orientation == 8 :
# Rotation 90
mirror = image . transpose ( Image . ROTATE_90 )
2014-03-16 15:19:52 +01:00
image = mirror
2014-03-16 15:17:13 +01:00
2011-05-06 00:37:15 +02:00
thumb_path = os . path . join ( thumb_path , image_cache ( self . _path , size , square ) )
2011-05-23 11:25:45 +02:00
info_string = " %s -> %s px " % ( os . path . basename ( original_path ) , str ( size ) )
if square :
info_string + = " , square "
message ( " thumbing " , info_string )
2011-05-07 08:56:54 +02:00
if os . path . exists ( thumb_path ) and file_mtime ( thumb_path ) > = self . _attributes [ " dateTimeFile " ] :
2011-05-05 14:42:04 +02:00
return
2011-05-06 08:49:26 +02:00
gc . collect ( )
2011-05-09 15:33:30 +02:00
try :
image = image . copy ( )
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-09 15:33:30 +02:00
except :
try :
image = image . copy ( ) # we try again to work around PIL bug
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-09 15:33:30 +02:00
except :
2011-05-23 11:25:45 +02:00
message ( " corrupt image " , os . path . basename ( original_path ) )
2011-05-09 15:33:30 +02:00
return
2011-05-05 14:42:04 +02:00
if square :
if image . size [ 0 ] > image . size [ 1 ] :
left = ( image . size [ 0 ] - image . size [ 1 ] ) / 2
top = 0
right = image . size [ 0 ] - ( ( image . size [ 0 ] - image . size [ 1 ] ) / 2 )
bottom = image . size [ 1 ]
else :
left = 0
top = ( image . size [ 1 ] - image . size [ 0 ] ) / 2
right = image . size [ 0 ]
bottom = image . size [ 1 ] - ( ( image . size [ 1 ] - image . size [ 0 ] ) / 2 )
image = image . crop ( ( left , top , right , bottom ) )
2011-05-06 09:19:20 +02:00
gc . collect ( )
2011-05-05 14:42:04 +02:00
image . thumbnail ( ( size , size ) , Image . ANTIALIAS )
2011-05-06 00:37:15 +02:00
try :
2012-04-22 19:20:54 +02:00
image . save ( thumb_path , " JPEG " , quality = 88 )
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
os . unlink ( thumb_path )
raise
2011-05-06 00:37:15 +02:00
except :
2011-05-23 11:25:45 +02:00
message ( " save failure " , os . path . basename ( thumb_path ) )
2011-05-07 13:42:34 +02:00
os . unlink ( thumb_path )
2011-05-05 14:42:04 +02:00
2014-03-16 15:19:52 +01:00
def _thumbnails ( self , original_path , thumb_path ) :
# get number of cores on the system, and use all minus one
num_of_cores = os . sysconf ( ' SC_NPROCESSORS_ONLN ' ) - 1
pool = Pool ( processes = num_of_cores )
for size in Photo . thumb_sizes :
pool . apply_async ( make_thumbs , args = ( self , original_path , thumb_path , size ) )
pool . close ( )
pool . join ( )
2014-03-16 15:17:13 +01:00
2011-05-05 13:04:40 +02:00
@property
def name ( self ) :
return os . path . basename ( self . _path )
def __str__ ( self ) :
return self . name
@property
2011-05-07 03:12:51 +02:00
def path ( self ) :
return self . _path
@property
2011-05-06 00:37:15 +02:00
def image_caches ( self ) :
return [ image_cache ( self . _path , size [ 0 ] , size [ 1 ] ) for size in Photo . thumb_sizes ]
@property
2011-05-05 13:04:40 +02:00
def date ( self ) :
2011-05-06 00:37:15 +02:00
if not self . is_valid :
return datetime ( 1900 , 1 , 1 )
2011-05-07 09:05:54 +02:00
if " dateTimeOriginal " in self . _attributes :
2011-05-07 09:00:06 +02:00
return self . _attributes [ " dateTimeOriginal " ]
2011-05-07 09:05:54 +02:00
elif " dateTime " in self . _attributes :
2011-05-07 09:00:06 +02:00
return self . _attributes [ " dateTime " ]
2011-05-05 13:04:40 +02:00
else :
2011-05-07 08:56:54 +02:00
return self . _attributes [ " dateTimeFile " ]
2011-05-05 13:04:40 +02:00
def __cmp__ ( self , other ) :
2011-05-06 01:49:57 +02:00
date_compare = cmp ( self . date , other . date )
if date_compare == 0 :
return cmp ( self . name , other . name )
return date_compare
2011-05-05 13:04:40 +02:00
@property
def attributes ( self ) :
return self . _attributes
@staticmethod
def from_dict ( dictionary , basepath ) :
del dictionary [ " date " ]
path = os . path . join ( basepath , dictionary [ " name " ] )
del dictionary [ " name " ]
2011-05-05 14:42:04 +02:00
for key , value in dictionary . items ( ) :
2011-05-07 09:12:20 +02:00
if key . startswith ( " dateTime " ) :
2011-05-05 14:42:04 +02:00
try :
dictionary [ key ] = datetime . strptime ( dictionary [ key ] , " %a % b %d % H: % M: % S % Y " )
2011-05-23 11:25:45 +02:00
except KeyboardInterrupt :
raise
2011-05-05 14:42:04 +02:00
except :
pass
2011-05-05 15:17:21 +02:00
return Photo ( path , None , dictionary )
2011-05-05 13:04:40 +02:00
def to_dict ( self ) :
photo = { " name " : self . name , " date " : self . date }
photo . update ( self . attributes )
return photo
class PhotoAlbumEncoder ( json . JSONEncoder ) :
def default ( self , obj ) :
if isinstance ( obj , datetime ) :
2011-05-05 14:42:04 +02:00
return obj . strftime ( " %a % b %d % H: % M: % S % Y " )
2011-05-05 13:04:40 +02:00
if isinstance ( obj , Album ) or isinstance ( obj , Photo ) :
return obj . to_dict ( )
return json . JSONEncoder . default ( self , obj )
2011-05-06 13:58:11 +02:00