2 # -*- coding: utf-8 -*-
4 #This file is part of screenlist project
6 #screenlist is a program that captures frames from a video file and save these frames as thumbnails in a single image file.
7 #Copyright (C) 2009 Apkawa
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #You can contact author by email Apkawa@gmail.com
31 import Image
, ImageFont
, ImageDraw
, ImageOps
38 #http://code.google.com/p/mplayer-snapshot/
39 #http://code.activestate.com/recipes/412982/
41 # get info file - mediainfo
42 # get screen - http://code.google.com/p/pyffmpeg/
43 # ffmpeg -i video.avi -an -ss 00:00:10 -r 1 -vframes 1 -s 320×240 -y -f mjpeg screenshot.jpg
47 s
.open_video('01.mkv')
48 #s.make_screens('tmp')
49 s
.SETTING
.title
.logo
= True
50 s
.make_screenlist(s
.GRID
,grid
=(6,6),)
51 s
.save_result('tmp/s.png')
53 def human_format(num
, suffix
='b'):
55 for x
in ['','K','M','G','T']:
57 return "%3.1f%s%s" % (num
, x
, suffix
)
60 def calc_resize( orig_wxh
, w
=None, h
=None ):
62 return int( ceil( __float
))
65 h
= ince( ( o_h
* w
) / float(o_w
) )
67 w
= ince( ( o_w
* h
) / float(o_h
) )
76 SETTING
= type('Visual_Setting',(),{
77 'canvas':type('Canvas_Setting',(),{
82 'frame': type('Frame_Setting',(),{
87 'border_color': '#AAA'
89 'title': type('Title_Setting',(),{
93 'logopath': 'logo.png',
95 '__doc__': 'Setting Screenlist class',
101 tempdir
= tempfile
.gettempdir()
104 self
.exec_mplayer_info
= 'mplayer "%s" \
109 self
.exec_mplayer
= 'mplayer "%(videopath)s" \
111 -vo %(oformat)s:outdir=%(tempdir)s \
122 self
.exec_mplayer_thumb
= self
.exec_mplayer
+ '-zoom -xy %(t_width)i '
124 def open_video(self
, video_path
):
125 'Open video and return bool'
126 self
.videopath
= video_path
127 __info
= os
.popen( self
.exec_mplayer_info
% video_path
).read()
129 self
.identify
= dict(re
.findall('ID_(.*)=(.*)',__info
))
130 self
.identify
.update({
131 'filesize': os
.stat( video_path
).st_size
133 if self
.identify
.get('LENGTH'):
135 self
.video_info
= re
.findall('VIDEO\:.*', __info
)[0]
136 _a
= re
.findall('AUDIO\:.*',__info
)
137 self
.audio_info
= [ _a
[0] if _a
else 'AUDIO: None']
140 __a_lang
= self
.identify
.get('AID_%i_LANG'%i)
141 __a_name
= self
.identify
.get('AID_%i_NAME'%i)
143 __a_str
= 'AID %i: %s %s'%(i
, __a_lang
, __a_name
if __a_name
else '')
144 self
.audio_info
.append( __a_str
)
147 #print self.video_info
148 #print self.audio_info
151 self
.video_length
= float(self
.identify
['LENGTH'])
158 def get_screen(self
, outfile
, time_pos
='00:00:00', t_width
=None):
160 'videopath':self
.videopath
,
162 'tempdir':self
.tempdir
,
163 'oformat':self
.oformat
,
166 __exec_arg
.update({'t_width':t_width
,})
168 self
.exec_mplayer_thumb
%__exec_arg
+'2>/dev/null'
172 self
.exec_mplayer
%__exec_arg
+'2>/dev/null'
174 screen
= os
.path
.join(self
.tempdir
,'00000001.png')
176 shutil
.copyfile( screen
, outfile
)
179 except ( OSError,IOError):
182 def make_screens(self
, outdir
,frames
=5, prefix
='',suffix
='' , t_width
=None):
183 if not os
.path
.isdir( outdir
):
184 raise Exception('%s not dir'%outdir
)
186 s_time
= int( self
.video_length
/ frames
)
188 for f
in xrange(frames
):
189 __filepath
= os
.path
.join(
190 os
.path
.abspath(outdir
),
191 '%s%s%s.%s'%(prefix
, f
+1, suffix
, self
.oformat
) )
192 time_pos
= self
.str_time(
193 s_time
*f
if f
else s_time
/3
195 if self
.get_screen( __filepath
, time_pos
, t_width
):
196 files
.append(__filepath
)
199 def make_screenlist(self
,
208 time_type - set method get frames of video.
209 self.FRAMES, self.SECONDS, self.GRID
210 t_width - width frame thumbinail
213 padding
= self
.SETTING
.canvas
.padding
214 fgcolor
= self
.SETTING
.canvas
.fgcolor
215 bgcolor
= self
.SETTING
.canvas
.bgcolor
218 if time_type
== self
.FRAMES
:
220 s_time
= int( self
.video_length
/frames
)
222 elif time_type
== self
.SECONDS
:
223 self
.frames
= int( self
.video_length
/ s_time
)
225 elif time_type
== self
.GRID
:
226 self
.frames
= grid
[0]*grid
[1]
227 s_time
= int( self
.video_length
/ self
.frames
)
230 for f
in xrange(self
.frames
):
231 __tempfile
= os
.path
.join( self
.tempdir
,'%i.png'%f)
232 time_pos
= self
.str_time(
233 s_time
*f
if f
else s_time
/3
238 if self
.get_screen( __tempfile
, time_pos
, t_width
):
239 self
.temp_files
.append( __tempfile
)
241 self
._make
_frame
( Image
.open( __tempfile
), time_pos
) )
247 def title( img
, text_tuple
,font
=None, title_height
=150, ):
248 logo
= self
.SETTING
.title
.logo
251 logo_img
= Image
.open( self
.SETTING
.title
.logopath
)
252 logo_img
= logo_img
.resize( calc_resize( logo_img
.size
, h
=title_height
-padding
*2) )
253 l_w
,l_h
= logo_img
.size
258 bbox
= (left
, top
, right
, bottom
)
259 img
.paste( logo_img
, bbox
)
261 logo_left
= marl
+l_w
+padding
264 fgcolor
= self
.SETTING
.title
.fgcolor
266 font
, fontsize
= self
._font
()
268 draw
= ImageDraw
.Draw( img
)
270 for r
in xrange(len( text_tuple
)):
273 xy
=(marl
+logo_left
,mart
+r
*(padding
+fontsize
)),
279 photow
, photoh
= self
.imgs
[0].size
280 #print photow, photoh
288 padw
= (columns
-1)*padding
289 rows
= int(ceil(self
.frames
/float(columns
)))
290 padh
= (rows
-1)*padding
291 image_size
= ( columns
*photow
+padw
+marw
, rows
*photoh
+padh
+marh
+title_h
)
293 self
.image_new
= Image
.new('RGB', image_size
, bgcolor
)
296 'Filename: %s; '%os.path
.split( self
.identify
['FILENAME'] )[1] +
297 'size: %s'% human_format( int(self
.identify
['filesize']) ),
298 # 'Resolution: %sx%s; '%( self.identify['VIDEO_WIDTH'],self.identify['VIDEO_HEIGHT'])+
299 'Duration: %s'%self
.str_time( self
.identify
['LENGTH']) ,
302 title_text
+= self
.audio_info
304 title( self
.image_new
, title_text
)
306 for r
in xrange(rows
):
307 for c
in xrange( columns
):
308 left
= marl
+c
*(photow
+padding
)
310 upper
= (mart
+r
*(photoh
+padding
))+title_h
312 bbox
= (left
, upper
, right
, lower
)
314 img
= self
.imgs
.pop(0)
315 self
.image_new
.paste(img
,bbox
)
321 def str_time(self
, seconds
):
322 'Return str_time in %H:%M:%S format'
323 return time
.strftime("%H:%M:%S", time
.gmtime(
324 int( float(seconds
) )
327 def _make_frame(self
, img
, str_time
,):
328 __frame
= self
.SETTING
.frame
329 padding
= __frame
.padding
330 bgcolor
= __frame
.bgcolor
331 fgcolor
= __frame
.fgcolor
332 border_color
= __frame
.border_color
333 font
, fontsize
= self
._font
( self
.SETTING
.frame
.fontsize
)
334 title_frame_height
= fontsize
+ padding
336 img
= ImageOps
.expand(img
,(padding
,padding
,padding
,title_frame_height
),fill
=bgcolor
)
337 img
= ImageOps
.expand(img
,(1,1,1,1),fill
= border_color
)
338 img_w
, img_h
= img
.size
339 _draw
= ImageDraw
.Draw( img
)
341 xy
=( padding
, img_h
-(title_frame_height
) ),
347 def _font(self
, fontsize
=18, fontpath
='fonts/arial.ttf'):
349 font
= ImageFont
.truetype( fontpath
,fontsize
)
351 font
= ImageFont
.load_default()
352 return font
, fontsize
354 def save_result(self
,dest
='tmp/screenlist.png'):
355 self
.image_new
.save(dest
, 'PNG')
358 for tf
in self
.temp_files
:
365 class CliAppScreenlist
:
377 def time_parse(option
, opt_str
, value
, __parser
, *args
, **kwargs
):
378 if not self
.time_type
:
379 self
.time_type
= kwargs
.get('time_type')
381 parser
.error('select only of -f,-s,-g')
382 if self
.time_type
== Screenlist
.FRAMES
:
383 self
.frames
= int(value
)
384 elif self
.time_type
== Screenlist
.SECONDS
:
385 self
.seconds
= int(value
)
386 elif self
.time_type
== Screenlist
.GRID
:
387 self
.grid
= re
.findall('^(\d+),(\d+)$',value
)
389 self
.grid
= [int(i
) for i
in self
.grid
[0]]
391 parser
.error('not valid values in %s. \d+,\d+'%option
)
392 if opt_str
== '--sh':
395 print option
, opt_str
, value
, args
, kwargs
398 parser
= optparse
.OptionParser()
399 parser
.add_option('-f','--frames', action
='callback', type='int',
400 callback
=time_parse
, callback_kwargs
={'time_type':Screenlist
.FRAMES
})
401 parser
.add_option('-s','--seconds', action
='callback', type='int' ,
402 callback
=time_parse
, callback_kwargs
={'time_type':Screenlist
.SECONDS
})
403 parser
.add_option('-g','--grid', action
='callback', type='string', metavar
='cols,rows',
404 callback
=time_parse
, callback_kwargs
={'time_type':Screenlist
.GRID
})
405 parser
.add_option('-o','--outdir', action
='store',
406 type='string',dest
='outdir',)
407 parser
.add_option('--sh','--screenshots', action
='store_true',dest
='screenshots')
409 (options
, self
.videofiles
) = parser
.parse_args()
410 #print options, self.videofiles
411 if not self
.videofiles
:
412 parser
.error('not select videofiles')
414 self
.outdir
= os
.path
.abspath( options
.outdir
)
415 if not os
.path
.isdir(self
.outdir
):
416 parser
.error('-o select dir name')
417 if options
.screenshots
:
418 self
.screenshots
= True
424 screenlist
= Screenlist()
425 #screenlist.SETTING.title.logo = True
426 for vf
in self
.videofiles
:
427 if os
.path
.isfile( vf
):
428 if not screenlist
.open_video(vf
):
429 os
.sys
.stderr
.write( 'Not open file %s\n'%vf
)
433 screenlist
.make_screenlist(
439 screenlist
.make_screenlist( screenlist
.GRID
)
440 video_dir
, video_name
= os
.path
.split(vf
)
444 save_dir
= self
.outdir
445 save_dir
=os
.path
.abspath( save_dir
)
446 result_path
= os
.path
.join( save_dir
, video_name
+'.png')
449 files
= screenlist
.make_screens( save_dir
, prefix
='screenshots_'+video_name
+'_')
450 print '\n'.join(files
)
452 if os
.path
.exists( result_path
):
455 if os
.path
.exists( result_path
):
456 result_path
= os
.path
.join(
457 save_dir
, video_name
+'-%i.png'%i)
463 screenlist
.save_result( result_path
)
471 if __name__
== '__main__':
472 c
= CliAppScreenlist()