summaryrefslogtreecommitdiff
path: root/rapid/thumbnail.py
diff options
context:
space:
mode:
Diffstat (limited to 'rapid/thumbnail.py')
-rw-r--r--rapid/thumbnail.py390
1 files changed, 390 insertions, 0 deletions
diff --git a/rapid/thumbnail.py b/rapid/thumbnail.py
new file mode 100644
index 0000000..e9c7e66
--- /dev/null
+++ b/rapid/thumbnail.py
@@ -0,0 +1,390 @@
+#!/usr/bin/python
+#!/usr/bin/python
+# -*- coding: latin1 -*-
+
+### Copyright (C) 2011 Damon Lynch <damonlynch@gmail.com>
+
+### This program is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+### GNU General Public License for more details.
+
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software
+### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import multiprocessing
+import types
+import os
+
+import gtk
+
+import paths
+
+from PIL import Image
+import cStringIO
+import tempfile
+import subprocess
+
+import rpdfile
+
+import rpdmultiprocessing as rpdmp
+from utilities import image_to_pixbuf, pixbuf_to_image
+import pyexiv2
+
+from filmstrip import add_filmstrip
+
+import logging
+logger = multiprocessing.get_logger()
+
+
+def get_stock_photo_image():
+ length = min(gtk.gdk.screen_width(), gtk.gdk.screen_height())
+ pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/photo.svg'), length, length)
+ image = pixbuf_to_image(pixbuf)
+ return image
+
+def get_stock_photo_image_icon():
+ image = Image.open(paths.share_dir('glade3/photo66.png'))
+ image = image.convert("RGBA")
+ return image
+
+def get_stock_video_image():
+ length = min(gtk.gdk.screen_width(), gtk.gdk.screen_height())
+ pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(paths.share_dir('glade3/video.svg'), length, length)
+ image = pixbuf_to_image(pixbuf)
+ return image
+
+def get_stock_video_image_icon():
+ image = Image.open(paths.share_dir('glade3/video66.png'))
+ image = image.convert("RGBA")
+ return image
+
+
+class PhotoIcons():
+ stock_thumbnail_image_icon = get_stock_photo_image_icon()
+
+class VideoIcons():
+ stock_thumbnail_image_icon = get_stock_video_image_icon()
+
+def upsize_pil(image, size):
+ width_max = size[0]
+ height_max = size[1]
+ width_orig = float(image.size[0])
+ height_orig = float(image.size[1])
+ if (width_orig / width_max) > (height_orig / height_max):
+ height = int((height_orig / width_orig) * width_max)
+ width = width_max
+ else:
+ width = int((width_orig / height_orig) * height_max)
+ height=height_max
+
+ return image.resize((width, height), Image.ANTIALIAS)
+
+def downsize_pil(image, box, fit=False):
+ """Downsample the PIL image.
+ image: Image - an Image-object
+ box: tuple(x, y) - the bounding box of the result image
+ fix: boolean - crop the image to fill the box
+
+ Code adpated from example by Christian Harms
+ Source: http://united-coders.com/christian-harms/image-resizing-tips-every-coder-should-know
+ """
+ #preresize image with factor 2, 4, 8 and fast algorithm
+ factor = 1
+ logger.debug("Image size %sx%s", image.size[0], image.size[1])
+ logger.debug("Box size %sx%s", box[0],box[1])
+ while image.size[0]/factor > 2*box[0] and image.size[1]*2/factor > 2*box[1]:
+ factor *=2
+ if factor > 1:
+ logger.debug("quick resize %sx%s", image.size[0]/factor, image.size[1]/factor)
+ image.thumbnail((image.size[0]/factor, image.size[1]/factor), Image.NEAREST)
+ logger.debug("did first thumbnail")
+
+ #calculate the cropping box and get the cropped part
+ if fit:
+ x1 = y1 = 0
+ x2, y2 = image.size
+ wRatio = 1.0 * x2/box[0]
+ hRatio = 1.0 * y2/box[1]
+ if hRatio > wRatio:
+ y1 = y2/2-box[1]*wRatio/2
+ y2 = y2/2+box[1]*wRatio/2
+ else:
+ x1 = x2/2-box[0]*hRatio/2
+ x2 = x2/2+box[0]*hRatio/2
+ image = image.crop((x1,y1,x2,y2))
+
+ #Resize the image with best quality algorithm ANTI-ALIAS
+ logger.debug("about to actually downsize using image.thumbnail")
+ image.thumbnail(box, Image.ANTIALIAS)
+ logger.debug("it downsized")
+
+class PicklablePIL:
+ def __init__(self, image):
+ self.size = image.size
+ self.mode = image.mode
+ self.image_data = image.tostring()
+
+ def get_image(self):
+ return Image.fromstring(self.mode, self.size, self.image_data)
+
+ def get_pixbuf(self):
+ return image_to_pixbuf(self.get_image())
+
+def get_video_THM_file(fullFileName):
+ """
+ Checks to see if a thumbnail file (THM) is in the same directory as the
+ file. Expects a full path to be part of the file name.
+
+ Returns the filename, including path, if found, else returns None.
+ """
+
+ f = None
+ name, ext = os.path.splitext(fullFileName)
+ for e in rpdfile.VIDEO_THUMBNAIL_EXTENSIONS:
+ if os.path.exists(name + '.' + e):
+ f = name + '.' + e
+ break
+ if os.path.exists(name + '.' + e.upper()):
+ f = name + '.' + e.upper()
+ break
+
+ return f
+
+class Thumbnail:
+
+ # file types from which to remove letterboxing (black bands in the thumbnail
+ # previews)
+ crop_thumbnails = ('CR2', 'DNG', 'RAF', 'ORF', 'PEF', 'ARW')
+
+ def _ignore_embedded_160x120_thumbnail(self, max_size_needed, metadata):
+ return max_size_needed is None or max_size_needed[0] > 160 or max_size_needed[1] > 120 or not metadata.exif_thumbnail.data
+
+ def _get_thumbnail_data(self, metadata, max_size_needed):
+ logger.debug("Getting thumbnail data %s", max_size_needed)
+ if self._ignore_embedded_160x120_thumbnail(max_size_needed, metadata):
+ logger.debug("Ignoring embedded preview")
+ lowrez = False
+ previews = metadata.previews
+ if not previews:
+ return (None, None)
+ else:
+ if max_size_needed:
+ for thumbnail in previews:
+ if thumbnail.dimensions[0] >= max_size_needed or thumbnail.dimensions[1] >= max_size_needed:
+ break
+ else:
+ thumbnail = previews[-1]
+ else:
+ thumbnail = metadata.exif_thumbnail
+ lowrez = True
+ return (thumbnail.data, lowrez)
+
+ def _process_thumbnail(self, image, size_reduced):
+ if image.mode <> "RGBA":
+ image = image.convert("RGBA")
+
+ thumbnail = PicklablePIL(image)
+ if size_reduced is not None:
+ thumbnail_icon = image.copy()
+ downsize_pil(thumbnail_icon, size_reduced, fit=False)
+ thumbnail_icon = PicklablePIL(thumbnail_icon)
+ else:
+ thumbnail_icon = None
+
+ return (thumbnail, thumbnail_icon)
+
+ def _get_photo_thumbnail(self, full_file_name, size_max, size_reduced):
+ thumbnail = None
+ thumbnail_icon = None
+ name = os.path.basename(full_file_name)
+ metadata = pyexiv2.metadata.ImageMetadata(full_file_name)
+ try:
+ logger.debug("Read photo metadata...")
+ metadata.read()
+ except:
+ logger.warning("Could not read metadata from %s", full_file_name)
+ else:
+ logger.debug("...successfully read photo metadata")
+ if metadata.mime_type == "image/jpeg" and self._ignore_embedded_160x120_thumbnail(size_max, metadata):
+ try:
+ image = Image.open(full_file_name)
+ lowrez = False
+ except:
+ logger.warning("Could not generate thumbnail for jpeg %s ", full_file_name)
+ image = None
+ else:
+ thumbnail_data, lowrez = self._get_thumbnail_data(metadata, max_size_needed=size_max)
+ logger.debug("_get_thumbnail_data returned")
+ if not isinstance(thumbnail_data, types.StringType):
+ image = None
+ else:
+ td = cStringIO.StringIO(thumbnail_data)
+ logger.debug("got td")
+ try:
+ image = Image.open(td)
+ except:
+ logger.warning("Unreadable thumbnail for %s", full_file_name)
+ image = None
+ logger.debug("opened image")
+ if image:
+ try:
+ orientation = metadata['Exif.Image.Orientation'].value
+ except:
+ orientation = None
+ if lowrez:
+ # need to remove letterboxing / pillarboxing from some
+ # RAW thumbnails
+ if os.path.splitext(full_file_name)[1][1:].upper() in Thumbnail.crop_thumbnails:
+ image2 = image.crop((0, 8, 160, 112))
+ image2.load()
+ image = image2
+ if size_max is not None and (image.size[0] > size_max[0] or image.size[1] > size_max[1]):
+ logger.debug("downsizing")
+ downsize_pil(image, size_max, fit=False)
+ logger.debug("downsized")
+ if orientation == 8:
+ # rotate counter clockwise
+ image = image.rotate(90)
+ elif orientation == 6:
+ # rotate clockwise
+ image = image.rotate(270)
+ elif orientation == 3:
+ # rotate upside down
+ image = image.rotate(180)
+ thumbnail, thumbnail_icon = self._process_thumbnail(image, size_reduced)
+
+ logger.debug("...got thumbnail for %s", full_file_name)
+ return (thumbnail, thumbnail_icon)
+
+ def _get_video_thumbnail(self, full_file_name, size_max, size_reduced):
+ thumbnail = None
+ thumbnail_icon = None
+ if size_max is None:
+ size = 0
+ else:
+ size = max(size_max[0], size_max[1])
+ image = None
+ if size > 0 and size <= 160:
+ thm = get_video_THM_file(full_file_name)
+ if thm:
+ try:
+ thumbnail = gtk.gdk.pixbuf_new_from_file(thm)
+ except:
+ logger.warning("Could not open THM file for %s", full_file_name)
+ thumbnail = add_filmstrip(thumbnail)
+ image = pixbuf_to_image(thumbnail)
+
+ if image is None:
+ try:
+ tmp_dir = tempfile.mkdtemp(prefix="rpd-tmp")
+ thm = os.path.join(tmp_dir, 'thumbnail.jpg')
+ subprocess.check_call(['ffmpegthumbnailer', '-i', full_file_name, '-t', '10', '-f', '-o', thm, '-s', str(size)])
+ image = Image.open(thm)
+ image.load()
+ os.unlink(thm)
+ os.rmdir(tmp_dir)
+ except:
+ image = None
+ logger.error("Error generating thumbnail for %s", full_file_name)
+ if image:
+ thumbnail, thumbnail_icon = self._process_thumbnail(image, size_reduced)
+
+ logger.debug("...got thumbnail for %s", full_file_name)
+ return (thumbnail, thumbnail_icon)
+
+ def get_thumbnail(self, full_file_name, file_type, size_max=None, size_reduced=None):
+ logger.debug("Getting thumbnail for %s...", full_file_name)
+ if file_type == rpdfile.FILE_TYPE_PHOTO:
+ logger.debug("file type is photo")
+ return self._get_photo_thumbnail(full_file_name, size_max, size_reduced)
+ else:
+ return self._get_video_thumbnail(full_file_name, size_max, size_reduced)
+
+
+class GetPreviewImage(multiprocessing.Process):
+ def __init__(self, results_pipe):
+ multiprocessing.Process.__init__(self)
+ self.daemon = True
+ self.results_pipe = results_pipe
+ self.thumbnail_maker = Thumbnail()
+ self.stock_photo_thumbnail_image = None
+ self.stock_video_thumbnail_image = None
+
+ def get_stock_image(self, file_type):
+ """
+ Get stock image for file type scaled to the current size of the
+ """
+ if file_type == rpdfile.FILE_TYPE_PHOTO:
+ if self.stock_photo_thumbnail_image is None:
+ self.stock_photo_thumbnail_image = PicklablePIL(get_stock_photo_image())
+ return self.stock_photo_thumbnail_image
+ else:
+ if self.stock_video_thumbnail_image is None:
+ self.stock_video_thumbnail_image = PicklablePIL(get_stock_video_image())
+ return self.stock_video_thumbnail_image
+
+ def run(self):
+ while True:
+ unique_id, full_file_name, file_type, size_max = self.results_pipe.recv()
+ full_size_preview, reduced_size_preview = self.thumbnail_maker.get_thumbnail(full_file_name, file_type, size_max=size_max, size_reduced=None)
+ if full_size_preview is None:
+ full_size_preview = self.get_stock_image(file_type)
+ self.results_pipe.send((unique_id, full_size_preview, reduced_size_preview))
+
+
+
+class GenerateThumbnails(multiprocessing.Process):
+ def __init__(self, files, batch_size, results_pipe, terminate_queue,
+ run_event):
+ multiprocessing.Process.__init__(self)
+ self.results_pipe = results_pipe
+ self.terminate_queue = terminate_queue
+ self.batch_size = batch_size
+ self.files = files
+ self.run_event = run_event
+ self.results = []
+
+ self.thumbnail_maker = Thumbnail()
+
+
+ def run(self):
+ counter = 0
+ i = 0
+ for f in self.files:
+
+ # pause if instructed by the caller
+ self.run_event.wait()
+
+ if not self.terminate_queue.empty():
+ x = self.terminate_queue.get()
+ # terminate immediately
+ logger.info("Terminating thumbnailing")
+ return None
+
+
+ thumbnail, thumbnail_icon = self.thumbnail_maker.get_thumbnail(
+ f.full_file_name,
+ f.file_type,
+ (160, 120), (100,100))
+
+ self.results.append((f.unique_id, thumbnail_icon, thumbnail))
+ counter += 1
+ if counter == self.batch_size:
+ self.results_pipe.send((rpdmp.CONN_PARTIAL, self.results))
+ self.results = []
+ counter = 0
+ i += 1
+
+ if counter > 0:
+ # send any remaining results
+ self.results_pipe.send((rpdmp.CONN_PARTIAL, self.results))
+ self.results_pipe.send((rpdmp.CONN_COMPLETE, None))
+ self.results_pipe.close()
+