1
# Copyright (c) 2009 Entertainer Developers - See COPYING - GPLv2
2
'''Thumbnailer classes for image and video.'''
3
# pylint: disable-msg=C0301
6
from threading import Event
14
from entertainerlib.exceptions import (ImageThumbnailerException,
15
ThumbnailerException, VideoThumbnailerException)
16
from entertainerlib.configuration import Configuration
19
class Thumbnailer(object):
20
'''Thumbnailer base class for video and image thumbnailers.'''
25
def __init__(self, filename, thumb_type):
27
self.config = Configuration()
28
thumb_dir = os.path.join(self.config.THUMB_DIR, thumb_type)
29
self.filename = filename
30
filehash = hashlib.md5()
31
filehash.update(self.filename)
32
self.filename_hash = filehash.hexdigest()
34
if not os.path.exists(self.filename):
35
raise ThumbnailerException(
36
'File to thumbnail does not exist : %s' % self.filename)
38
if os.path.exists(thumb_dir):
39
if os.path.isfile(filename):
40
self._thumb_file = os.path.join(thumb_dir,
41
self.filename_hash + '.jpg')
43
raise ThumbnailerException(
44
'Thumbnailer filename is a folder : %s' % self.filename)
46
raise ThumbnailerException('Unknown thumbnail type : %s' % (
50
'''Get the hash of the filename'''
51
return self.filename_hash
53
def create_thumbnail(self):
55
Implement this method in deriving classes.
57
Method should create a new thumbnail and save it to the Entertainer's
58
thumbnail directory in JPEG format. Thumbnail filename should be a MD5
59
hash of the absolute path of the original file.
61
raise NotImplementedError
64
class ImageThumbnailer(Thumbnailer):
65
"""Thumbnailer for image files."""
67
def __init__(self, filename):
68
"""Create a new Image thumbnailer"""
69
Thumbnailer.__init__(self, filename, 'image')
71
self.im = Image.open(self.filename)
73
raise ImageThumbnailerException(
74
'Error while opening file : %s' % self.filename)
77
def create_thumbnail(self):
79
Method creates a new thumbnail and saves it to the Entertainer's
80
thumbnail directory in PNG format. Thumbnail filename is a MD5
81
hash of the given filename.
84
# Calculate new size here
85
original_width = self.im.size[0]
86
original_height = self.im.size[1]
87
if original_width <= self.MAX_SIZE and (
88
original_height <= self.MAX_SIZE):
90
self.im.save(self._thumb_file, "JPEG",
91
quality=self.THUMB_QUALITY)
93
raise ImageThumbnailerException('Error saving thumbnail')
96
if original_width > original_height:
98
height = (width * original_height) / original_width
100
height = self.MAX_SIZE
101
width = (height * original_width) / original_height
103
self.im.thumbnail((width, height), Image.ANTIALIAS)
104
self.im.save(self._thumb_file, "JPEG",
105
quality=self.THUMB_QUALITY)
107
raise ImageThumbnailerException('Error saving thumbnail')
110
class VideoThumbnailer(Thumbnailer):
111
'''Create thumbnails from videos.'''
112
# I think it's important to note that the methodology of this code (and
113
# some actual EXACT snippets of code) were based on Elisa
114
# (http://elisa.fluendo.com/)
117
class VideoSinkBin(gst.Bin):
118
'''A gstreamer sink bin'''
120
def __init__(self, needed_caps):
122
gst.Bin.__init__(self)
123
self._capsfilter = gst.element_factory_make(
124
'capsfilter', 'capsfilter')
126
self.set_caps(needed_caps)
127
self.add(self._capsfilter)
129
fakesink = gst.element_factory_make('fakesink', 'fakesink')
130
fakesink.set_property("sync", False)
132
self._capsfilter.link(fakesink)
134
pad = self._capsfilter.get_pad("sink")
135
ghostpad = gst.GhostPad("sink", pad)
137
pad2probe = fakesink.get_pad("sink")
138
pad2probe.add_buffer_probe(self.buffer_probe)
140
self.add_pad(ghostpad)
141
self.sink = self._capsfilter
143
def set_current_frame(self, value):
144
'''Set the current frame'''
145
self._current_frame = value
147
def set_caps(self, caps):
148
'''Set the bin caps'''
149
gst_caps = gst.caps_from_string(caps)
150
self._capsfilter.set_property("caps", gst_caps)
152
def get_current_frame(self):
153
'''Gets the current frame'''
154
frame = self._current_frame
155
self._current_frame = None
158
def buffer_probe(self, pad, buff):
159
'''Buffer the probe'''
163
self.width = s['width']
164
self.height = s['height']
165
if self.width != None and self.height != None and buff != None:
166
self.set_current_frame(buff.data)
173
self.set_current_frame(None)
176
def __init__(self, filename, src="video"):
178
Thumbnailer.__init__(self, filename, src)
179
self._fileuri = 'file://%s' % (self.filename)
181
#Initialize and use the gstreamer pipeline
182
self._pipeline = gst.element_factory_make('playbin', 'playbin')
183
self._sink = self.VideoSinkBin('video/x-raw-rgb,bpp=24,depth=24')
184
self._blocker = Event()
185
self._pipeline.set_property('video-sink', self._sink)
186
self._pipeline.set_property('volume', 0)
189
self.BORING_THRESHOLD = 2000
190
self.HOLES_SIZE = (9, 35)
191
self.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'
193
def add_holes(self, img):
194
'''Add the holes image'''
195
holes = Image.fromstring('RGBA', self.HOLES_SIZE, self.HOLES_DATA)
196
holes_h = holes.size[1]
197
remain = img.size[1] % holes_h
201
while i < (img.size[1] - remain):
202
left_box = (0, i, holes.size[0], (nbands+1) * holes.size[1])
203
img.paste(holes, left_box)
205
right_box = (img.size[0] - holes.size[0], i,
206
img.size[0], (nbands+1) * holes.size[1])
207
img.paste(holes, right_box)
212
remain_holes = holes.crop((0, 0, holes.size[0], remain))
214
img.paste(remain_holes, (0, i, holes.size[0], img.size[1]))
215
img.paste(remain_holes, (img.size[0] - holes.size[0], i,
216
img.size[0], img.size[1]))
219
def interesting_image(self, img):
221
Checks an image to see if it has the characteristics of an
222
'interesting' image, i.e. whether or not the image is worth
223
examining for thumbnailing
225
stat = ImageStat.Stat(img)
226
return True in [ i > self.BORING_THRESHOLD for i in stat.var ]
228
def set_pipeline_state(self, pipeline, state):
230
Does exactly what it claims : sets the state of the pipeline, and
231
returns the boolean value of whether or not is successfully changed
234
status = pipeline.set_state(state)
235
if status == gst.STATE_CHANGE_ASYNC:
240
while not result[0] == gst.STATE_CHANGE_SUCCESS:
245
result = pipeline.get_state(50*gst.MSECOND)
248
elif status == gst.STATE_CHANGE_SUCCESS:
254
def create_thumbnail(self):
256
Creates the new thumbnail based on the information provided through the
257
gstreamer API. The file is an md5 of the video's file name, followed
258
by the .jpg extension
261
if os.path.exists(self._thumb_file):
264
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
265
self._pipeline.set_property('uri', self._fileuri)
267
if not self.set_pipeline_state(self._pipeline, gst.STATE_PAUSED):
268
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
269
raise VideoThumbnailerException('Cannot start the pipeline')
271
if self._sink.width == None or self._sink.height == None:
272
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
273
raise VideoThumbnailerException('Unable to determine media size')
274
sink_size = (self._sink.width, self._sink.height)
277
duration = self._pipeline.query_duration(gst.FORMAT_TIME)[0]
278
except AssertionError:
279
#Gstreamer cannot determine the media duration using
280
#playing-thumbnailing for file
281
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
283
img = self._play_for_thumb(sink_size, 0)
285
img.save(self._thumb_file)
288
duration /= gst.NSECOND
290
img = self._seek_for_thumb(duration, sink_size)
293
img.save(self._thumb_file)
295
except VideoThumbnailerException:
296
#Fallback: No Image found in seek_for, falling back to
298
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
299
img = self._play_for_thumb(sink_size, duration)
300
#Fallback-Play found img
302
img.save(self._thumb_file)
305
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
306
raise VideoThumbnailerException(
307
'Unable to create thumbnail. Please file a bug')
310
def _play_for_thumb(self, sink_size, duration=0):
312
Plays the video file to gather information for generating a thumbnail
316
if duration >= 250000:
318
elif duration >= 200000:
320
elif duration >= 10000:
322
elif duration >= 5000:
327
#Setting every-frame to self._every
329
self._every_co = self._every
331
## How often Proceed?
334
self.set_state_blocking(self._pipeline, gst.STATE_PLAYING)
337
self._pipeline.set_state(gst.STATE_NULL)
340
def _seek_for_thumb(self, duration, sink_size):
342
Seeks through the video file to gather information for generating a
345
frame_locations = [ 1.0 / 3.0, 2.0 / 3.0, 0.1, 0.9, 0.5 ]
347
for location in frame_locations:
348
abs_location = int(location * duration)
350
if abs_location == 0:
351
raise VideoThumbnailerException(
352
self.filename, 'Video has a size of zero')
354
event = self._pipeline.seek(1.0, gst.FORMAT_TIME,
355
gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT,
356
gst.SEEK_TYPE_SET, abs_location,
357
gst.SEEK_TYPE_NONE, 0)
359
raise VideoThumbnailerException(self.filename,
360
'Unable to seek through video')
362
if not self.set_pipeline_state(self._pipeline, gst.STATE_PAUSED):
363
raise VideoThumbnailerException(self.filename,
364
'Unable to pause video')
366
frame = self._sink.get_current_frame()
368
img = Image.frombuffer(
369
"RGB", sink_size, frame, "raw", "RGB", 0, 1)
371
if self.interesting_image(img):
379
img.thumbnail((self.MAX_SIZE, self.MAX_SIZE), Image.BILINEAR)
380
if img.mode != 'RGBA':
381
img = img.convert(mode='RGBA')
382
self.set_pipeline_state(self._pipeline, gst.STATE_NULL)
385
gobject.type_register(VideoThumbnailer.VideoSinkBin)