diff --git a/PhotoAlbum.py b/PhotoAlbum.py index 0bbbb09..a8fe71d 100644 --- a/PhotoAlbum.py +++ b/PhotoAlbum.py @@ -6,6 +6,8 @@ from PIL.ExifTags import TAGS def set_cache_path_base(base): trim_base.base = base +def untrim_base(path): + return os.path.join(trim_base.base, path) def trim_base(path): if path.startswith(trim_base.base): path = path[len(trim_base.base):] @@ -41,7 +43,7 @@ class Album(object): def date(self): self._sort() if len(self._photos) == 0 and len(self._albums) == 0: - return datetime.min + return datetime(1900, 1, 1) elif len(self._photos) == 0: return self._albums[-1].date elif len(self._albums) == 0: @@ -77,7 +79,7 @@ class Album(object): def from_dict(dictionary, cripple=True): album = Album(dictionary["path"]) for photo in dictionary["photos"]: - album.add_photo(Photo.from_dict(photo, album.path)) + album.add_photo(Photo.from_dict(photo, untrim_base(album.path))) if not cripple: for subalbum in dictionary["albums"]: album.add_album(Album.from_dict(subalbum), cripple) @@ -90,46 +92,104 @@ class Album(object): else: subalbums = self._albums return { "path": self.path, "date": self.date, "albums": subalbums, "photos": self._photos } + def photo_from_path(self, path): + for photo in self._photos: + if trim_base(path) == photo._path: + print "cache hit %s" % path + return photo + return None class Photo(object): - def __init__(self, path, attributes=None): + def __init__(self, path, thumb_path=None, attributes=None): self._path = trim_base(path) self.is_valid = True mtime = datetime.fromtimestamp(os.path.getmtime(path)) - if attributes is not None and attributes["FileTime"] >= mtime: + if attributes is not None and attributes["DateTimeFile"] >= mtime: self._attributes = attributes return self._attributes = {} - self._attributes["FileTime"] = mtime + self._attributes["DateTimeFile"] = mtime try: - i = Image.open(path) + image = Image.open(path) except: self.is_valid = False return + self._metadata(image) + self._thumbnails(image, thumb_path) + def _metadata(self, image): try: - info = i._getexif() + info = image._getexif() except: - info = None - if info: - for tag, value in info.items(): - decoded = TAGS.get(tag, tag) - if not isinstance(decoded, int) and decoded not in ['JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'FileSource', 'MakerNote', 'UserComment', 'ImageDescription', 'ComponentsConfiguration']: - if isinstance(value, str): - value = value.strip() - if decoded.startswith("DateTime"): - try: - value = datetime.strptime(value, '%Y:%m:%d %H:%M:%S') - except: - pass - self._attributes[decoded] = value + return + for tag, value in info.items(): + decoded = TAGS.get(tag, tag) + if not isinstance(decoded, int) and decoded not in ['JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'FileSource', 'MakerNote', 'UserComment', 'ImageDescription', 'ComponentsConfiguration']: + if isinstance(value, str): + value = value.strip() + if decoded.startswith("DateTime"): + try: + value = datetime.strptime(value, '%Y:%m:%d %H:%M:%S') + except: + pass + self._attributes[decoded] = value + def _thumbnail(self, image, thumb_path, size, square=False): + if square: + suffix = str(size) + "s" + else: + suffix = str(size) + thumb_path = os.path.join(thumb_path, image_cache(self._path, suffix)) + if os.path.exists(thumb_path) and datetime.fromtimestamp(os.path.getmtime(thumb_path)) >= self._attributes["DateTimeFile"]: + return + image = image.copy() + 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)) + image.thumbnail((size, size), Image.ANTIALIAS) + image.save(thumb_path, "JPEG") + print "saving %s" % thumb_path + + def _thumbnails(self, image, thumb_path): + orientation = self._attributes["Orientation"] + mirror = image + if orientation == 2: + # Vertical Mirror + mirror = image.transpose(Image.FLIP_LEFT_RIGHT) + elif orientation == 3: + # Rotation 180 + mirror = image.transpose(Image.ROTATE_180) + elif orientation == 4: + # Horizontal Mirror + mirror = image.transpose(Image.FLIP_TOP_BOTTOM) + elif orientation == 5: + # Horizontal Mirror + Rotation 270 + mirror = image.transpose(Image.FLIP_TOP_BOTTOM).transpose(Image.ROTATE_270) + elif orientation == 6: + # Rotation 270 + mirror = image.transpose(Image.ROTATE_270) + elif orientation == 7: + # Vertical Mirror + Rotation 270 + mirror = image.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_270) + elif orientation == 8: + # Rotation 90 + mirror = image.transpose(Image.ROTATE_90) + self._thumbnail(mirror, thumb_path, 100, True) + self._thumbnail(mirror, thumb_path, 640) + self._thumbnail(mirror, thumb_path, 1024) @property def name(self): return os.path.basename(self._path) def __str__(self): return self.name - def cache_path(self, size): - return image_cache(self.path, size) @property def date(self): if "DateTimeOriginal" in self._attributes: @@ -137,7 +197,7 @@ class Photo(object): elif "DateTime" in self._attributes: return self._attributes["DateTime"] else: - return self._attributes["FileTime"] + return self._attributes["DateTimeFile"] def __cmp__(self, other): return cmp(self.date, other.date) @property @@ -148,6 +208,12 @@ class Photo(object): del dictionary["date"] path = os.path.join(basepath, dictionary["name"]) del dictionary["name"] + for key, value in dictionary.items(): + if key.startswith("DateTime"): + try: + dictionary[key] = datetime.strptime(dictionary[key], "%a %b %d %H:%M:%S %Y") + except: + pass return Photo(path, dictionary) def to_dict(self): photo = { "name": self.name, "date": self.date } @@ -157,7 +223,7 @@ class Photo(object): class PhotoAlbumEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): - return obj.isoformat() + return obj.strftime("%a %b %d %H:%M:%S %Y") if isinstance(obj, Album) or isinstance(obj, Photo): return obj.to_dict() return json.JSONEncoder.default(self, obj) diff --git a/TreeWalker.py b/TreeWalker.py index 1ec1cba..e8b9e76 100644 --- a/TreeWalker.py +++ b/TreeWalker.py @@ -1,5 +1,6 @@ import os import os.path +from datetime import datetime from PhotoAlbum import Photo, Album, json_cache, set_cache_path_base class TreeWalker: @@ -14,17 +15,27 @@ class TreeWalker: def walk(self, path): cache = os.path.join(self.cache_path, json_cache(path)) cached = False - if os.path.exists(cache) and os.path.getmtime(path) <= os.path.getmtime(cache): - album = Album.from_cache(cache) - cached = True - else: + cached_album = None + if os.path.exists(cache): + cached_album = Album.from_cache(cache) + if os.path.getmtime(path) <= os.path.getmtime(cache): + cached = True + album = cached_album + if not cached: album = Album(path) for entry in os.listdir(path): entry = os.path.join(path, entry) if os.path.isdir(entry): album.add_album(self.walk(entry)) elif not cached and os.path.isfile(entry): - photo = Photo(entry) + cache_hit = False + if cached_album: + cached_photo = cached_album.photo_from_path(entry) + if cached_photo and datetime.fromtimestamp(os.path.getmtime(entry)) <= cached_photo.attributes["DateTimeFile"]: + cache_hit = True + photo = cached_photo + if not cache_hit: + photo = Photo(entry, self.cache_path) if photo.is_valid: self.all_photos.append(photo) album.add_photo(photo)