Package elisa :: Package core :: Module thumbnailer
[hide private]
[frames] | no frames]

Source Code for Module elisa.core.thumbnailer

  1  # -*- coding: utf-8 -*- 
  2  # Elisa - Home multimedia server 
  3  # Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com). 
  4  # All rights reserved. 
  5  # 
  6  # This file is available under one of two license agreements. 
  7  # 
  8  # This file is licensed under the GPL version 3. 
  9  # See "LICENSE.GPL" in the root of this distribution including a special 
 10  # exception to use Elisa with Fluendo's plugins. 
 11  # 
 12  # The GPL part of Elisa is also available under a commercial licensing 
 13  # agreement from Fluendo. 
 14  # See "LICENSE.Elisa" in the root directory of this distribution package 
 15  # for details on that license. 
 16   
 17   
 18  __maintainer__ = 'Florian Boucault <florian@fluendo.com>' 
 19  __maintainer2__ = 'Benjamin Kampmann <benjamin@fluendo.com>' 
 20   
 21  from elisa.core import common 
 22   
 23  from elisa.extern.log import log 
 24   
 25  from twisted.internet import defer, reactor, threads 
 26  from twisted.python.failure import Failure 
 27   
 28  import PIL 
 29  from PIL import PngImagePlugin 
 30  import StringIO 
 31   
 32  import gst 
 33   
 34  import gobject 
 35  from mutex import mutex 
 36  import sys, os 
 37  import time 
 38  import Queue 
 39  import md5 #FIXME: deprecated in 2.5, but hashlib is not available on 2.4 
 40  import Image, ImageStat 
 41   
 42  BORING_IMAGE_VARIANCE=2000 
 43   
 44  HOLES_SIZE= (9, 35) 
 45  HOLES_DATA='\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x9a\x9a\x9a\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x91\x91\x91\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\xff\xff\xff\xa6\x91\x91\x91\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6\x00\x00\x00\xa6' 
 46   
47 -class ThumbnailerError(Exception):
48
49 - def __init__(self, uri, error_message=None):
50 if error_message == None: 51 output = "Failed thumbnailing %r" % uri 52 else: 53 output = "Failed thumbnailing %r: %r" % (uri, error_message) 54 55 Exception.__init__(self, output) 56 self.uri = uri 57 self.error_message = error_message
58
59 -class NoThumbnailerFound(Exception):
60 pass
61
62 -class VideoSinkBin(gst.Bin):
63
64 - def __init__(self, needed_caps):
65 self.reset() 66 gst.Bin.__init__(self) 67 self._capsfilter = gst.element_factory_make('capsfilter', 'capsfilter') 68 69 self.set_caps(needed_caps) 70 self.add(self._capsfilter) 71 72 fakesink = gst.element_factory_make('fakesink', 'fakesink') 73 fakesink.set_property("sync", False) 74 self.add(fakesink) 75 self._capsfilter.link(fakesink) 76 77 pad = self._capsfilter.get_pad("sink") 78 ghostpad = gst.GhostPad("sink", pad) 79 80 pad2probe = fakesink.get_pad("sink") 81 pad2probe.add_buffer_probe(self.buffer_probe) 82 83 self.add_pad(ghostpad) 84 self.sink = self._capsfilter
85
86 - def set_current_frame(self, value):
87 self._current_frame = value
88
89 - def set_caps(self, caps):
90 gst_caps = gst.caps_from_string(caps) 91 self._capsfilter.set_property("caps", gst_caps)
92
93 - def get_current_frame(self):
94 frame = self._current_frame 95 self._current_frame = None 96 return frame
97
98 - def buffer_probe(self, pad, buffer):
99 caps = buffer.caps 100 if caps != None: 101 s = caps[0] 102 self.width = s['width'] 103 self.height = s['height'] 104 if self.width != None and self.height != None and buffer != None: 105 self.set_current_frame(buffer.data) 106 return True
107
108 - def reset(self):
109 self.width = None 110 self.height = None 111 self.set_current_frame(None)
112 113 114 gobject.type_register(VideoSinkBin) 115 116 from threading import Event 117
118 -class VideoThumbnailer(log.Loggable):
119 120 logCategory = "thumbnailer" 121
122 - def __init__(self):
123 self._pipeline = gst.element_factory_make('playbin', 'playbin') 124 caps = "video/x-raw-rgb,bpp=24,depth=24" 125 126 self._sink = VideoSinkBin(caps) 127 self._blocker = Event() 128 129 self._pipeline.set_property("video-sink", self._sink) 130 self._pipeline.set_property('volume', 0)
131 132
133 - def get_holes_img(self):
134 img = Image.fromstring('RGBA', HOLES_SIZE, HOLES_DATA) 135 return img
136
137 - def add_holes(self, img):
138 holes = self.get_holes_img() 139 holes_h = holes.size[1] 140 remain = img.size[1] % holes_h 141 142 i = 0 143 nbands = 0 144 while i < (img.size[1] - remain): 145 left_box = (0, i, holes.size[0], (nbands+1) * holes.size[1]) 146 img.paste(holes, left_box) 147 148 right_box = (img.size[0] - holes.size[0], i, 149 img.size[0], (nbands+1) * holes.size[1]) 150 img.paste(holes, right_box) 151 152 i += holes_h 153 nbands += 1 154 155 remain_holes = holes.crop((0, 0, holes.size[0], remain)) 156 remain_holes.load() 157 img.paste(remain_holes, (0, i, holes.size[0], img.size[1])) 158 img.paste(remain_holes, (img.size[0] - holes.size[0], i, 159 img.size[0], img.size[1])) 160 return img
161
162 - def interesting_image(self, img):
163 stat = ImageStat.Stat(img) 164 return True in [ i > BORING_IMAGE_VARIANCE for i in stat.var ]
165
166 - def set_state_blocking(self, pipeline, state):
167 status = pipeline.set_state(state) 168 if status == gst.STATE_CHANGE_ASYNC: 169 self.debug("Waiting for state change completion to %s..." % state) 170 171 result = [False] 172 max_try = 100 173 nb_try = 0 174 while not result[0] == gst.STATE_CHANGE_SUCCESS: 175 if nb_try > max_try: 176 self.debug("State change failed: %s" % result[0]) 177 return False 178 nb_try += 1 179 result = pipeline.get_state(50*gst.MSECOND) 180 181 self.debug("State change completed.") 182 return True 183 elif status == gst.STATE_CHANGE_SUCCESS: 184 self.debug("State change completed.") 185 return True 186 else: 187 self.debug("State change failed") 188 return False
189 190
191 - def generate_thumbnail(self, video_uri, size):
192 """ 193 Try to generate a thumbnail for the video located at video_uri, 194 of size width. 195 196 @param video_uri: URI to make a thumbnail from 197 @type video_uri: L{elisa.core.media_uri.MediaUri} 198 @param size: size of the thumbnail in pixels 199 @type size: int 200 201 @raise ThumbnailerError: if an error occurs when generating the thumbnail 202 """ 203 204 self.set_state_blocking(self._pipeline, gst.STATE_NULL) 205 self.debug("Generating thumbnail for file: %s" % video_uri) 206 self._pipeline.set_property('uri', video_uri) 207 208 """ 209 def bus_event(bus, message, pipeline): 210 t = message.type 211 if t == gst.MESSAGE_EOS: 212 self.debug("End of Stream") 213 pass 214 elif t == gst.MESSAGE_ERROR: 215 err, debug = message.parse_error() 216 self.debug("Error: %s %s" % (err, debug)) 217 self.set_state_blocking(pipeline, gst.STATE_NULL) 218 return True 219 220 self._pipeline.get_bus().add_watch(bus_event, self._pipeline) 221 """ 222 223 # start the pipeline 224 if not self.set_state_blocking(self._pipeline, gst.STATE_PAUSED): 225 self.debug("Cannot start the pipeline") 226 self.set_state_blocking(self._pipeline, gst.STATE_NULL) 227 raise ThumbnailerError(video_uri) 228 229 if self._sink.width == None or self._sink.height == None: 230 self.debug("Cannot determine media size") 231 self.set_state_blocking(self._pipeline, gst.STATE_NULL) 232 raise ThumbnailerError(video_uri) 233 sink_size = (self._sink.width, self._sink.height) 234 235 self.debug("width: %s; height: %s" % (self._sink.width, self._sink.height)) 236 try: 237 duration, format = self._pipeline.query_duration(gst.FORMAT_TIME) 238 except Exception, e: 239 ## FIXME: precise this exception 240 self.debug("Gstreamer cannot determine the media duration." 241 " using playing-thumbnailing for %s" % video_uri) 242 self.set_state_blocking(self._pipeline, gst.STATE_NULL) 243 img = self._play_for_thumb(sink_size, size, 0) 244 self.debug("play found %s" % img) 245 if img: 246 return img 247 else: 248 duration /= gst.NSECOND 249 self.debug("duration: %s" % duration) 250 try: 251 img = self._seek_for_thumb(video_uri, duration, sink_size, size) 252 self.debug("seek found %s" % img) 253 if img: 254 return img 255 except ThumbnailerError, e: 256 # Fallback: No Image found in seek_for 257 self.debug("Using Fallback: play_for_thumb") 258 self.set_state_blocking(self._pipeline, gst.STATE_NULL) 259 img = self._play_for_thumb(sink_size, size, duration) 260 self.debug("Fallback-Play found %s" % img) 261 if img: 262 return img 263 # stop the pipeline 264 self.set_state_blocking(self._pipeline, gst.STATE_NULL) 265 raise ThumbnailerError(video_uri)
266 267
268 - def _play_for_thumb(self, sink_size, size, duration=0):
269 ## Maybe we should set a timer, so that we don't play the whole movie? 270 self.debug("Doing play_for_thumb!") 271 self.debug(duration) 272 id = None 273 self._img = None 274 275 if duration >= 250000: 276 self._every = 25 277 elif duration >= 200000: 278 self._every = 15 279 elif duration >= 10000: 280 self._every = 10 281 elif duration >= 5000: 282 self._every = 5 283 else: 284 self._every = 1 285 286 self.debug("Setting every-frame to %s" % self._every) 287 288 self._every_co = self._every 289 290 ## How often Proceed? 291 self._counter = 5 292 293 def buffer_probe(pad, buffer): 294 ## Proceed only every 5th frame! 295 if self._every_co < self._every: 296 self._every_co += 1 297 return 298 self._every_co = 0 299 self.debug("Proceeding a Frame") 300 301 try: 302 img = Image.frombuffer("RGB", sink_size, buffer, 303 "raw", "RGB",0, 1) 304 except Exception, e: 305 self.debug("Invalid frame") 306 else: 307 self.debug("Found Frame") 308 self._img = img 309 if self.interesting_image(self._img): 310 self.debug("Intresting image found") 311 312 self._img.thumbnail((size, size), Image.BILINEAR) 313 if img.mode != 'RGBA': 314 img = img.convert(mode='RGBA') 315 316 self.debug("releasing %s" % self._img) 317 self._sink.reset() 318 pad.remove_buffer_probe(id) 319 self._blocker.set() 320 return 321 322 self._counter -= 1 323 if self._counter <= 0: 324 self.debug("Counter off, resetting blocker!") 325 # Is it better to return no image instead of a 'boring' one? 326 if self._img: 327 self._img.thumbnail((size, size), Image.BILINEAR) 328 if img.mode != 'RGBA': 329 img = img.convert(mode='RGBA') 330 331 # self._img = None 332 self._sink.reset() 333 pad.remove_buffer_probe(id) 334 self._blocker.set()
335 336 337 self.debug("Setting Pipeline") 338 self.set_state_blocking(self._pipeline, gst.STATE_PLAYING) 339 # self._pipeline.set_state(gst.STATE_PLAYING) 340 pad = self._sink.get_pad('sink') 341 id = pad.add_buffer_probe(buffer_probe) 342 self.debug("Wait") 343 self._blocker.wait() 344 self.debug("Going on") 345 self._pipeline.set_state(gst.STATE_NULL) 346 self.debug("returning %s" % self._img) 347 return self._img
348
349 - def _seek_for_thumb(self, video_uri, duration, sink_size, size):
350 frame_locations = [ 1.0 / 3.0, 2.0 / 3.0, 0.1, 0.9, 0.5 ] 351 self.debug('seeking') 352 353 for location in frame_locations: 354 abs_location = int(location * duration) 355 self.debug("location: rel %s, abs %s" % (location, abs_location)) 356 357 if abs_location == 0: 358 raise ThumbnailerError(video_uri, "Empty media") 359 360 event = self._pipeline.seek(1.0, gst.FORMAT_TIME, 361 gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, 362 gst.SEEK_TYPE_SET, abs_location, 363 gst.SEEK_TYPE_NONE, 0) 364 if not event: 365 raise ThumbnailerError(video_uri, "Not Seekable") 366 367 if not self.set_state_blocking(self._pipeline, gst.STATE_PAUSED): 368 raise ThumbnailerError(video_uri, "Not Pausable") 369 370 frame = self._sink.get_current_frame() 371 372 try: 373 img = Image.frombuffer("RGB", sink_size, frame, "raw", "RGB", 0, 1) 374 except: 375 self.debug("Invalid frame") 376 continue 377 378 if self.interesting_image(img): 379 self.debug("Interesting image found") 380 break 381 else: 382 self.debug("Image not interesting") 383 pass 384 385 self._sink.reset() 386 387 if img: 388 img.thumbnail((size, size), Image.BILINEAR) 389 if img.mode != 'RGBA': 390 img = img.convert(mode='RGBA') 391 self.set_state_blocking(self._pipeline, gst.STATE_NULL) 392 return img
393 394
395 -class Thumbnailer(log.Loggable):
396 397 logCategory = "thumbnailer" 398
399 - def __init__(self, thumbnail_dir=None):
400 """ 401 402 @param thumbnail_dir: abolute path to a directory where to store thumbnails 403 @type thumbnail_dir: string 404 """ 405 406 if not thumbnail_dir: 407 thumbnail_dir = os.path.join(os.path.expanduser("~"), ".thumbnails") 408 self._thumbnail_dir = thumbnail_dir 409 410 self._video_thumbnailer = VideoThumbnailer() 411 self._queue = Queue.Queue() 412 self._running = False 413 self._delayed_call = None
414
415 - def stop(self):
416 if self._delayed_call and self._delayed_call.active(): 417 self._delayed_call.cancel()
418 419
420 - def _save_thumbnail_as(self, file_uri, thumbnail, thumbnail_filename):
421 """ 422 Save a thumbnail of an URI at a given location. 423 424 @param file_uri: URI the thumbnail was generated from 425 @type file_uri: L{elisa.core.media_uri.MediaUri} 426 @param thumbnail: Thumbnail instance 427 @type thumbnail: L{PIL.Image} 428 @param thumbnail_filename: filename to save to 429 @type thumbnail_filename: string 430 @raises ThumbnailerError: if the thumbnail couldn't be saved 431 """ 432 433 # if the thumbnail directory does not exist yet, create it 434 directory = os.path.dirname(thumbnail_filename) 435 if not os.path.exists(directory): 436 try: 437 os.makedirs(directory, 0700) 438 except OSError, e: 439 msg = "Could not make directory %r: %s. Thumbnail not saved." % (directory, e) 440 self.warning(msg) 441 raise ThumbnailerError(file_uri, msg) 442 443 info = PngImagePlugin.PngInfo() 444 445 # required metadata 446 info.add_text("Thumb::URI", str(file_uri)) 447 # FIXME: requires network transparent mtime 448 #info.add_text("Thumb::MTime", os.path.getmtime(file_uri)) 449 450 # TODO: think about some useful (but optional) metadata to add 451 # cf: http://jens.triq.net/thumbnailnail-spec/creation.html 452 453 # FIXME: access permissions on thumbnail must be set to 600 454 thumbnail.save(thumbnail_filename, "png", pnginfo=info)
455
456 - def _get_thumbnail_location(self, file_uri, size):
457 """ 458 Return the thumbnail's location of a file for a particular size. 459 460 @param file_uri: URI of the file 461 @type file_uri: L{elisa.core.media_uri.MediaUri} 462 @param size: Size of the thumbnail (in pixels) 463 @type size: int 464 @rtype: tuple (path to thumbnail, maximum thumbnail size) 465 466 @raise Exception: Raise an exception if size is superior to 512 467 """ 468 # FIXME: if file_uri refers to ~/.thumbnails/*, return str(file_uri) 469 470 if size <= 128: 471 thumbnail_dir_size = "normal" 472 thumbnail_max_size = 128 473 elif size <= 256: 474 thumbnail_dir_size = "large" 475 thumbnail_max_size = 256 476 elif size <= 512: 477 # NOTE: this is not supported by the freedesktop specification 478 thumbnail_dir_size = "extra_large" 479 thumbnail_max_size = 512 480 else: 481 raise Exception("ThumbnailManager._get_thumbnail_location(): size \ 482 given too large: %d" % size) 483 484 thumbnail_filename = md5.new(str(file_uri)).hexdigest() + ".png" 485 486 return (os.path.join(self._thumbnail_dir, thumbnail_dir_size, 487 thumbnail_filename), 488 thumbnail_max_size)
489 490
491 - def _get_thumbnail_from(self, file_uri, size):
492 """ 493 Retrieve a thumbnail from a given URI. 494 495 @param file_uri: URI to get the thumbnail from 496 @param file_uri: L{elisa.core.media_uri.MediaUri} 497 @param size: size of the thumbnail in pixels 498 @type size: int 499 500 @rtype: L{PIL.Image} or None 501 502 @raise: Exception raise if an error occurs while loading 503 """ 504 thumbnail_path, size = self._get_thumbnail_location(file_uri, size) 505 506 if os.path.exists(thumbnail_path): 507 # the thumbnail file exists and is valid, just load it 508 try: 509 thumbnail = image.Image(thumbnail_path) 510 return thumbnail 511 except IOError, error: 512 raise Exception("Error loading %s: %s" % (thumbnail_path, error)) 513 else: 514 return None
515 516
517 - def _retrieve_thumbnail(self, uri, size, media_type):
518 """ 519 @raise ThumbnailerError: if an error occurs when generating the 520 thumbnail 521 @raise NoThumbnailerFound: when it is not possible to generate the 522 thumbnail 523 """ 524 self.debug("Generating thumbnail for %s of %s type" % (uri, media_type)) 525 if media_type == 'image': 526 dfr = defer.Deferred() 527 self._queue.put( (uri, size, dfr) ) 528 if not self._running: 529 self._running = True 530 self._delayed_call = reactor.callLater(0.01, 531 self._process_next) 532 return dfr 533 534 # elif media_type == 'video': 535 # img = self._video_thumbnailer.generate_thumbnail(uri, size) 536 else: 537 return defer.fail(NoThumbnailerFound( 538 "No thumbnailer available for type %s\ 539 (%s)" % (media_type, uri)))
540
541 - def _process_next(self):
542 # Stop processing 543 if self._queue.empty(): 544 self._running = False 545 return 546 547 self._running = True 548 uri, size, item_dfr = self._queue.get() 549 media_manager = common.application.media_manager 550 551 def do_thumbnail(data, filename, size): 552 img = Image.open(StringIO.StringIO(data)) 553 img.thumbnail((size, size), Image.ANTIALIAS) 554 # Save the thumbnail 555 self._save_thumbnail_as(uri, img, filename)
556 557 def thumbnail_done(result, thumbnail_filename, thumbnail_max_size): 558 self.debug('returning (%s, %s)' % (thumbnail_filename, 559 thumbnail_max_size)) 560 item_dfr.callback((thumbnail_filename, thumbnail_max_size)) 561 self._delayed_call = reactor.callLater(0.01, self._process_next)
562 563 def got_error(failure): 564 self._delayed_call = reactor.callLater(0.01, self._process_next) 565 self.debug('Error while processing %s:%s' % (uri, failure)) 566 567 def got_readed_data(data): 568 self.debug('got media data') 569 thumbnail_filename, thumbnail_max_size = self._get_thumbnail_location(uri, size) 570 dfr = threads.deferToThread(do_thumbnail, 571 data, thumbnail_filename, thumbnail_max_size) 572 dfr.addCallback(thumbnail_done, 573 thumbnail_filename, thumbnail_max_size) 574 575 return dfr 576 577 def open_done(media_file): 578 if media_file is None: 579 self.debug("Didn't get a media_file :(") 580 raise Exception("Didn't get a media_file, reading failed") 581 self.debug('got media file %s' % media_file) 582 dfr = media_file.read() 583 dfr.addCallback(got_readed_data) 584 dfr.addErrback(got_error) 585 return dfr 586 587 dfr = media_manager.open(uri) 588 dfr.addCallback(open_done) 589 dfr.addErrback(got_error) 590
591 - def get_thumbnail(self, uri, size, media_type):
592 """ 593 Creates a thumbnail of uri on a local location. 594 Returns a deferred, it's callback is called with 595 a tuple containing the local filename of the thumbnail 596 and its size as an int. 597 Calls the deferred error callback if an error occured. 598 599 @param uri: URI to generate a thumbnail from 600 @type uri: L{elisa.core.media_uri.MediaUri} 601 @param size: size of the desired thumbnail 602 @type size: int 603 @rtype: L{twisted.internet.defer.Deferred} 604 """ 605 self.debug("Requesting thumbnail for %s of %s type" \ 606 % (uri, media_type)) 607 uri = common.application.media_manager.get_real_uri(uri) 608 f, size = self._get_thumbnail_location(uri, size) 609 610 # Create the thumbnail if not already available 611 if os.path.exists(f): 612 self.debug("Returning already existing thumbnail for %s" % uri) 613 return defer.succeed( (f, size) ) 614 615 return self._retrieve_thumbnail(uri, size,media_type)
616
617 - def add_thumbnail(self, uri, size, thumbnail):
618 """ 619 Adds to the thumbnail cache a thumbnail created by 620 a 3rd party component (like a View) 621 622 @param uri: URI the thumbnail has been generated from 623 @type uri: L{elisa.core.media_uri.MediaUri} 624 @param size: size of the thumbnail 625 @type size: int 626 @param thumbnail: the Image 627 @type thumbnail: L{PIL.Image} 628 """ 629 630 if thumbnail: 631 thumbnail_filename, thumbnail_max_size = self._get_thumbnail_location(uri, size) 632 self._save_thumbnail_as(uri, thumbnail, thumbnail_filename)
633 634 635 636 #if __name__ == "__main__": 637 # 638 # from elisa.core import media_uri 639 # from elisa.core import common 640 # 641 # common.boot() 642 # 643 # 644 # t = Thumbnailer() 645 # path = u'/home/haiku/tmp/plier.mpeg' 646 # size = 512 647 # media_type = 'video' 648 # uri = media_uri.MediaUri(path) 649 # 650 # def done(pic): 651 # 652 # if pic: 653 # print "thumbnail generated !" 654 # else: 655 # print "the thumbnail has NOT been generated" 656 # 657 # def error(msg): 658 # 659 # print "error occured:", msg.getTraceback() 660 # 661 # dfr = t.get_thumbnail(uri, size, media_type) 662 # dfr.addCallback(done) 663 # dfr.addErrback(error) 664 # 665 # try: 666 # from twisted.internet import glib2reactor 667 # glib2reactor.install() 668 # except AssertionError: 669 # # already installed... 670 # pass 671 # 672 # from twisted.internet import reactor 673 # 674 # reactor.run() 675