vcs_ruby 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # Video Contact Sheet Ruby:
4
+ # ----------------------
5
+ #
6
+ # Generates contact sheets of videos
7
+ #
8
+ # Prerequisites: Ruby, ImageMagick, ffmpeg/libav or mplayer
9
+ #
10
+
11
+ # Load library path for development
12
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), "../lib")
13
+
14
+ require 'optparse'
15
+ require 'vcs'
16
+ require 'yaml'
17
+
18
+ include VCSRuby
19
+
20
+
21
+ # Configuration can Override options
22
+ options =
23
+ {
24
+ quiet: false,
25
+ verbose: false,
26
+ capturer: :any,
27
+ format: nil,
28
+ output: []
29
+ }
30
+
31
+ # Command Line Parameter arguments
32
+
33
+ arguments =
34
+ {
35
+ '--capturer' => [:ffmpeg, :libav, :mplayer, :any],
36
+ '--format' => [:png, :jpg, :jpeg, :tiff],
37
+ '--funky' => [:polaroid, :photos, :overlap, :rotate, :photoframe, :polaroidframe, :film, :random]
38
+ }
39
+
40
+ # Command Line Parameters
41
+ optparse = OptionParser.new do|opts|
42
+ opts.separator $vcs_ruby_name + ' ' + $vcs_ruby_version.to_s
43
+ opts.separator ''
44
+ opts.on( '-i [INTERVAL]', '--interval [INTERVAL]', 'Set the interval [INTERVAL]') do |interval|
45
+ options[:interval] = TimeIndex.new interval
46
+ end
47
+ opts.on( '-c [COLMNS]', '--columns [COLUMNS]', 'Arrange the output in <COLUMNS> columns.') do |columns|
48
+ options[:columns] = columns.to_i
49
+ end
50
+ opts.on( '-r [ROWS]', '--rows [ROWS]', 'Arrange the output in <ROWS> rows.') do |rows|
51
+ options[:rows] = rows.to_i
52
+ end
53
+ opts.on( '-H [HEIGHT]', '--height [HEIGHT]', 'Set the output (individual thumbnail) height.') do |height|
54
+ options[:height] = height.to_i
55
+ end
56
+ opts.on( '-W [WIDTH]', '--width [WIDTH]', 'Set the output (individual thumbnail) width.') do |width|
57
+ options[:width] = width.to_i
58
+ end
59
+ opts.on( '-A [ASPECT]', '--aspect [ASPECT]', 'Aspect ratio. Accepts a floating point number or a fraction.') do |aspect|
60
+ options[:aspect] = aspect.to_f
61
+ end
62
+ opts.on( '-f [FROM]', '--from [FROM]', 'Set starting time. No caps before this.') do |from|
63
+ options[:from] = TimeIndex.new from
64
+ end
65
+ opts.on( '-t [TO]', '--to [TO]', 'Set ending time. No caps beyond this.') do |to|
66
+ options[:to] = TimeIndex.new to
67
+ end
68
+ opts.on( '-T [TITLE]', '--title [TITLE]', 'Set ending time. No caps beyond this.') do |title|
69
+ options[:title] = title
70
+ end
71
+ opts.on( '-f [format]', '--format [FORMAT]', arguments['--format'], 'Formats: ' + Tools::list_arguments(arguments["--format"])) do |format|
72
+ options[:format] = format
73
+ end
74
+ opts.on('-C [CAPTURER]', '--capture [CAPTURER]', arguments['--capturer'], 'Capturer: ' + Tools::list_arguments(arguments["--capturer"])) do |capturer|
75
+ options[:capturer] = capturer
76
+ end
77
+ opts.on( '-T [TITLE]', '--title [TITLE]', 'Set ending time. No caps beyond this.') do |title|
78
+ options[:title] = title
79
+ end
80
+ opts.on( '-o [FILE]', '--output [FILE]', 'File name of output. When ommited will be derived from the input filename. Can be repeated for multiple files.') do |file|
81
+ options[:output] << file
82
+ end
83
+ opts.on( '-s [SIGNATURE]', '--signature [SIGNATURE]', 'Change the image signature to your preference.') do |signature|
84
+ options[:signature] = signature
85
+ end
86
+ opts.on( '--no-signature', 'Remove footer with signature') do
87
+ options[:no_signature] = true
88
+ end
89
+ opts.on( '-l [HIGHLIGHT]', '--highlight [HIGHLIGHT]' 'Add the frame found at timestamp [HIGHLIGHT] as a highlight.') do |highlight|
90
+ options[:highlight] = TimeIndex.new highlight
91
+ end
92
+ opts.on( '-q', '--quiet', 'Don\'t print progress messages just errors. Repeat to mute completely, even on error.') do |file|
93
+ options[:quiet] = true
94
+ end
95
+ opts.on("-V", "--verbose", "More verbose Output.") do
96
+ options[:verbose] = true
97
+ end
98
+ opts.on( '-v', '--version', 'Version' ) do
99
+ puts $vcs_ruby_name + ' ' + $vcs_ruby_version.to_s
100
+ exit 0
101
+ end
102
+
103
+ opts.on( '-h', '--help', 'Prints help' ) do
104
+ options[:help] = true
105
+ end
106
+
107
+ opts.separator ''
108
+ opts.separator 'Examples:'
109
+ opts.separator ' Create a contact sheet with default values (4 x 4 matrix):'
110
+ opts.separator ' $ vcs video.avi'
111
+ opts.separator ''
112
+ opts.separator ' Create a sheet with vidcaps at intervals of 3 and a half minutes, save to'
113
+ opts.separator ' "output.jpg":'
114
+ opts.separator ' $ vcs -i 3m30 input.wmv -o output.jpg'
115
+ opts.separator ''
116
+ opts.separator ' Create a sheet with vidcaps starting at 3 mins and ending at 18 mins in 2m intervals'
117
+ opts.separator ' $ vcs --from 3m --to 18m -i 2m input.avi'
118
+ opts.separator ''
119
+ opts.separator ' See more examples at vcs-ruby homepage <https://github.com/FreeApophis/vcs.rb>.'
120
+ opts.separator ''
121
+ end
122
+
123
+ Tools::print_help optparse if ARGV.empty?
124
+
125
+ optparse.parse!
126
+
127
+ puts options.inspect
128
+
129
+ Tools::print_help optparse if options[:help] || ARGV.empty?
130
+
131
+ Tools::verbose = options[:verbose]
132
+ Tools::quiet = options[:quiet]
133
+
134
+ # Invoke ContactSheet
135
+
136
+ begin
137
+ ARGV.each_with_index do |video, index|
138
+ sheet = Tools::contact_sheet_with_options video, options
139
+ sheet.initialize_filename(options[:output][index]) if options[:output][index]
140
+ sheet.build
141
+ end
142
+ rescue Exception => e
143
+ STDERR.puts "ERROR: #{e.message}"
144
+ end
145
+
@@ -0,0 +1,35 @@
1
+ #
2
+ # Capturer Baseclass
3
+ #
4
+
5
+ module VCSRuby
6
+ class Capturer
7
+ def available?
8
+ false
9
+ end
10
+
11
+ def name
12
+ raise "NotImplmentedException"
13
+ end
14
+
15
+ def load_video
16
+ raise "NotImplmentedException"
17
+ end
18
+
19
+ def length
20
+ raise "NotImplmentedException"
21
+ end
22
+
23
+ def width
24
+ raise "NotImplmentedException"
25
+ end
26
+
27
+ def height
28
+ raise "NotImplmentedException"
29
+ end
30
+
31
+ def grab time
32
+ raise "NotImplmentedException"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # Command
3
+ #
4
+
5
+ module VCSRuby
6
+ class Command
7
+ attr_reader :name, :available
8
+ def initialize name, command
9
+ @name = name
10
+ @command = which(command)
11
+ @available = !!@command
12
+ end
13
+
14
+ def execute parameter, streams = "2> /dev/null"
15
+ raise "Command '#{name}' not available" unless available
16
+
17
+ if Tools::windows?
18
+ `cmd /C #{@command} #{parameter}`
19
+ else
20
+ `#{@command} #{parameter} #{streams}`
21
+ end
22
+ end
23
+
24
+ private
25
+ # http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
26
+ def which cmd
27
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
28
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
29
+ exts.each do |ext|
30
+ exe = File.join(path, "#{cmd}#{ext}")
31
+ return exe if File.executable?(exe) && !File.directory?(exe)
32
+ end
33
+ end
34
+ return nil
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,103 @@
1
+ #
2
+ # Configuration
3
+ #
4
+
5
+ require 'font'
6
+
7
+ module VCSRuby
8
+ class Configuration
9
+ attr_accessor :capturer
10
+ attr_reader :header_font, :title_font, :timestamp_font, :signature_font
11
+
12
+ def initialize
13
+ default_config_file = File.expand_path("defaults.yml", File.dirname(__FILE__))
14
+ local_config_files = ['~/.vcs.rb.yml']
15
+
16
+ config = ::YAML::load_file(default_config_file)
17
+ local_config_files.select{ |f| File.exists?(f) }.each do |local_config_file|
18
+ puts "Local configuration file loaded: #{local_config_file}" if Tools.verbose?
19
+ local_config = YAML::load_file(local_config_file)
20
+ cconfig.merge(local_config)
21
+ end
22
+
23
+ @config = config
24
+
25
+ @header_font = Font.new @config['style']['header']['font'], @config['style']['header']['size']
26
+ @title_font = Font.new @config['style']['title']['font'], @config['style']['title']['size']
27
+ @timestamp_font = Font.new @config['style']['timestamp']['font'], @config['style']['timestamp']['size']
28
+ @signature_font = Font.new @config['style']['signature']['font'], @config['style']['signature']['size']
29
+ end
30
+
31
+ def rows
32
+ @config['main']['rows'] ? @config['main']['rows'].to_i : nil
33
+ end
34
+
35
+ def columns
36
+ @config['main']['columns'] ? @config['main']['columns'].to_i : nil
37
+ end
38
+
39
+ def interval
40
+ @config['main']['interval'] ? TimeIndex.new(@config['main']['interval']) : nil
41
+ end
42
+
43
+ def padding
44
+ @config['main']['padding'] ? @config['main']['padding'].to_i : 2
45
+ end
46
+
47
+ def quality
48
+ @config['main']['quality'] ? @config['main']['quality'].to_i : 90
49
+ end
50
+
51
+ def header_background
52
+ @config['style']['header']['background']
53
+ end
54
+
55
+ def header_color
56
+ @config['style']['header']['color']
57
+ end
58
+
59
+ def title_background
60
+ @config['style']['title']['background']
61
+ end
62
+
63
+ def title_color
64
+ @config['style']['title']['color']
65
+ end
66
+
67
+ def highlight_background
68
+ @config['style']['highlight']['background']
69
+ end
70
+
71
+ def contact_background
72
+ @config['style']['contact']['background']
73
+ end
74
+
75
+ def timestamp_background
76
+ @config['style']['timestamp']['background']
77
+ end
78
+
79
+ def timestamp_color
80
+ @config['style']['timestamp']['color']
81
+ end
82
+
83
+ def signature_background
84
+ @config['style']['signature']['background']
85
+ end
86
+
87
+ def signature_color
88
+ @config['style']['signature']['color']
89
+ end
90
+
91
+ def blank_threshold
92
+ @config['lowlevel']['blank_threshold'].to_f
93
+ end
94
+
95
+ def blank_evasion?
96
+ @config['lowlevel']['blank_evasion']
97
+ end
98
+
99
+ def blank_alternatives
100
+ @config['lowlevel']['blank_alternatives'].map{ |e| TimeIndex.new e.to_i }
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,341 @@
1
+ #
2
+ # Contact Sheet Composited from the Thumbnails
3
+ #
4
+
5
+ require 'fileutils'
6
+ require 'tmpdir'
7
+ require 'yaml'
8
+
9
+ require 'vcs'
10
+
11
+ module VCSRuby
12
+ class ContactSheet
13
+ attr_accessor :capturer, :format, :signature, :title, :highlight
14
+ attr_reader :thumbnail_width, :thumbnail_height
15
+ attr_reader :length, :from, :to
16
+
17
+ def initialize video, capturer = :any
18
+ @capturer = capturer
19
+ @configuration = Configuration.new
20
+ @signature = "Created by Video Contact Sheet Ruby"
21
+ initialize_capturers video
22
+ initialize_filename(File.basename(@video, '.*'))
23
+ puts "Processing #{File.basename(video)}..." unless Tools.quiet?
24
+ detect_video_properties
25
+
26
+ @thumbnails = []
27
+
28
+ @tempdir = Dir.mktmpdir
29
+
30
+ ObjectSpace.define_finalizer(self, self.class.finalize(@tempdir) )
31
+
32
+ initialize_geometry(@configuration.rows, @configuration.columns, @configuration.interval)
33
+ end
34
+
35
+ def initialize_filename filename
36
+ @out_path = File.dirname(filename)
37
+ @out_filename = File.basename(filename,'.*')
38
+ ext = File.extname(filename).gsub('.', '')
39
+ if ['png', 'jpg', 'jpeg', 'tiff'].include?(ext)
40
+ @format ||= ext.to_sym
41
+ end
42
+ end
43
+
44
+ def filename
45
+ "#{@out_filename}.#{@format ? @format.to_s : 'png'}"
46
+ end
47
+
48
+ def full_path
49
+ File.join(@out_path, filename)
50
+ end
51
+
52
+ def initialize_geometry(rows, columns, interval)
53
+ @has_interval = !!interval
54
+ @rows = rows
55
+ @columns = columns
56
+ @interval = interval
57
+ end
58
+
59
+ def rows
60
+ @rows
61
+ end
62
+
63
+ def columns
64
+ @columns
65
+ end
66
+
67
+ def interval
68
+ @interval || (@to - @from) / (number_of_caps + 1)
69
+ end
70
+
71
+ def number_of_caps
72
+ if @has_interval
73
+ (@to - @from) / @interval
74
+ else
75
+ if @rows && @columns
76
+ @rows * @columns
77
+ else
78
+ raise "you need at least 2 parameters from columns, rows and interval"
79
+ end
80
+ end
81
+ end
82
+
83
+ def thumbnail_width= width
84
+ @thumbnail_height = (width.to_f / @thumbnail_width * thumbnail_height).to_i
85
+ @thumbnail_width = width
86
+ end
87
+
88
+ def thumbnail_height= height
89
+ @thumbnail_width = (height.to_f / @thumbnail_height * thumbnail_width).to_i
90
+ @thumbnail_height = height
91
+ end
92
+
93
+ def from= time
94
+ if (TimeIndex.new(0) < time) && (time < to) && (time < @length)
95
+ @from = time
96
+ else
97
+ raise "Invalid From Time"
98
+ end
99
+ end
100
+
101
+ def to= time
102
+ if (TimeIndex.new(0) < time) && (from < time) && (time < @length)
103
+ @to = time
104
+ else
105
+ raise "Invalid To Time"
106
+ end
107
+ end
108
+
109
+
110
+ def self.finalize(tempdir)
111
+ proc do
112
+ puts "Cleaning up..." unless Tools.quiet?
113
+ FileUtils.rm_r tempdir
114
+ end
115
+ end
116
+
117
+ def build
118
+ initialize_thumbnails
119
+ capture_thumbnails
120
+
121
+ puts "Composing standard contact sheet..." unless Tools.quiet?
122
+ s = splice_montage(montage_thumbs)
123
+
124
+ image = MiniMagick::Image.open(s)
125
+
126
+ puts "Adding header and footer..." unless Tools.quiet?
127
+ final = add_header_and_footer image
128
+
129
+ puts "Done. Output wrote to '#{filename}'" unless Tools.quiet?
130
+ FileUtils.mv(final, full_path)
131
+ end
132
+
133
+
134
+ private
135
+ def selected_capturer
136
+ result = nil
137
+ if @capturer == nil || @capturer == :any
138
+ result = @capturers.first
139
+ else
140
+ result = @capturers.select{ |c| c.name == @capturer }.first
141
+ end
142
+ raise "Selected Capturer (#{@capturer.to_s}) not available" unless result
143
+ return result
144
+ end
145
+
146
+ def initialize_capturers video
147
+ capturers = []
148
+ capturers << LibAV.new(video)
149
+ capturers << MPlayer.new(video)
150
+ capturers << FFmpeg.new(video)
151
+
152
+ @video = video
153
+ @capturers = capturers.select{ |c| c.available? }
154
+
155
+ puts "Available capturers: #{@capturers.map{ |c| c.to_s }.join(', ')}" if Tools.verbose?
156
+ end
157
+
158
+ def initialize_thumbnails
159
+ time = @from
160
+ (1..number_of_caps).each do |i|
161
+ thumb = Thumbnail.new selected_capturer, @video, @configuration
162
+
163
+ thumb.width = thumbnail_width
164
+ thumb.height = thumbnail_height
165
+ thumb.time = (time += interval)
166
+ thumb.image_path = File::join(@tempdir, "th#{"%03d" % i}.png")
167
+
168
+ @thumbnails << thumb
169
+ end
170
+ end
171
+
172
+ def capture_thumbnails
173
+ puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Tools.quiet?
174
+
175
+ @thumbnails.each_with_index do |thumbnail, i|
176
+ puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Tools::quiet?
177
+ if @configuration.blank_evasion?
178
+ thumbnail.capture_and_evade interval
179
+ else
180
+ thumbnail.capture
181
+ end
182
+ thumbnail.apply_filters
183
+ end
184
+ end
185
+
186
+ def detect_video_properties
187
+ detect_length
188
+ detect_dimensions
189
+ end
190
+
191
+ def detect_length
192
+ @length = selected_capturer.length
193
+
194
+ @from = TimeIndex.new 0.0
195
+ @to = @length
196
+ end
197
+
198
+ def detect_dimensions
199
+ @thumbnail_width = selected_capturer.width
200
+ @thumbnail_height = selected_capturer.height
201
+ end
202
+
203
+ def montage_thumbs
204
+ file_path = File::join(@tempdir, 'montage.png')
205
+ MiniMagick::Tool::Montage.new do |montage|
206
+ montage.background @configuration.contact_background
207
+ @thumbnails.each do |thumbnail|
208
+ montage << thumbnail.image_path
209
+ end
210
+ montage.geometry "+#{@configuration.padding}+#{@configuration.padding}"
211
+ # rows or columns can be nil (auto fit)
212
+ montage.tile "#{@columns}x#{@rows}"
213
+ montage << file_path
214
+ end
215
+ return file_path
216
+ end
217
+
218
+ def splice_montage montage_path
219
+ file_path = File::join(@tempdir, 'spliced.png')
220
+ MiniMagick::Tool::Convert.new do |convert|
221
+ convert << montage_path
222
+ convert.background @configuration.contact_background
223
+ convert.splice '5x10'
224
+ convert << file_path
225
+ end
226
+ file_path
227
+ end
228
+
229
+ def create_title montage
230
+ file_path = File::join(@tempdir, 'title.png')
231
+ MiniMagick::Tool::Convert.new do |convert|
232
+ convert.stack do |ul|
233
+ ul.size "#{montage.width}x#{@configuration.title_font.line_height}"
234
+ ul.xc @configuration.title_background
235
+ ul.font @configuration.title_font.path
236
+ ul.pointsize @configuration.title_font.size
237
+ ul.background @configuration.title_background
238
+ ul.fill @configuration.title_color
239
+ ul.gravity 'Center'
240
+ ul.annotate(0, @title)
241
+ end
242
+ convert.flatten
243
+ convert << file_path
244
+ end
245
+ return file_path
246
+ end
247
+
248
+ def create_highlight montage
249
+ puts "Generating highlight..."
250
+ thumb = Thumbnail.new selected_capturer, @video, @configuration
251
+
252
+ thumb.width = thumbnail_width
253
+ thumb.height = thumbnail_height
254
+ thumb.time = @highlight
255
+ thumb.image_path = File::join(@tempdir, "highlight_thumb.png")
256
+ thumb.capture
257
+ thumb.apply_filters
258
+
259
+ file_path = File::join(@tempdir, "highlight.png")
260
+ MiniMagick::Tool::Convert.new do |convert|
261
+ convert.stack do |a|
262
+ a.size "#{montage.width}x#{thumbnail_height+20}"
263
+ a.xc @configuration.highlight_background
264
+ a.gravity 'Center'
265
+ a << thumb.image_path
266
+ a.composite
267
+ end
268
+ convert.stack do |a|
269
+ a.size "#{montage.width}x1"
270
+ a.xc 'Black'
271
+ end
272
+ convert.append
273
+ convert << file_path
274
+ end
275
+
276
+ file_path
277
+ end
278
+
279
+ def add_header_and_footer montage
280
+ file_path = File::join(@tempdir, filename)
281
+ header_height = @configuration.header_font.line_height * 3
282
+ signature_height = @configuration.signature_font.line_height + 8
283
+ MiniMagick::Tool::Convert.new do |convert|
284
+ convert.stack do |a|
285
+ a.size "#{montage.width - 18}x1"
286
+ a.xc @configuration.header_background
287
+ a.size.+
288
+ a.font @configuration.header_font.path
289
+ a.pointsize @configuration.header_font.size
290
+ a.background @configuration.header_background
291
+ a.fill 'Black'
292
+ a.stack do |b|
293
+ b.gravity 'West'
294
+ b.stack do |c|
295
+ c.label 'Filename: '
296
+ c.font @configuration.header_font.path
297
+ c.label File.basename(@video)
298
+ c.append.+
299
+ end
300
+ b.font @configuration.header_font.path
301
+ b.label "File size: #{Tools.to_human_size(File.size(@video))}"
302
+ b.label "Length: #{@length.to_timestamp}"
303
+ b.append
304
+ b.crop "#{montage.width}x#{header_height}+0+0"
305
+ end
306
+ a.append
307
+ a.stack do |b|
308
+ b.size "#{montage.width}x#{header_height}"
309
+ b.gravity 'East'
310
+ b.fill @configuration.header_color
311
+ b.annotate '+0-1'
312
+ b << "Dimensions: #{selected_capturer.width}x#{selected_capturer.height}\nFormat: #{selected_capturer.video_codec} / #{selected_capturer.audio_codec}\nFPS: #{"%.02f" % selected_capturer.fps}"
313
+ end
314
+ a.bordercolor @configuration.header_background
315
+ a.border 9
316
+ end
317
+ convert << create_title(montage) if @title
318
+ convert << create_highlight(montage) if @highlight
319
+ convert << montage.path
320
+ convert.append
321
+ if @signature
322
+ convert.stack do |a|
323
+ a.size "#{montage.width}x#{signature_height}"
324
+ a.gravity 'Center'
325
+ a.xc @configuration.signature_background
326
+ a.font @configuration.signature_font.path
327
+ a.pointsize @configuration.signature_font.size
328
+ a.fill @configuration.signature_color
329
+ a.annotate(0, @signature)
330
+ end
331
+ convert.append
332
+ end
333
+ if format == :jpg || format == :jpeg
334
+ convert.quality(@configuration.quality)
335
+ end
336
+ convert << file_path
337
+ end
338
+ file_path
339
+ end
340
+ end
341
+ end
@@ -0,0 +1,35 @@
1
+ main:
2
+ rows: 4
3
+ columns: 4
4
+ interval: ~
5
+ padding: 2
6
+ quality: 95
7
+ style:
8
+ header:
9
+ font: DejaVu-Sans-Book
10
+ size: 14
11
+ color: Black
12
+ background: "#afcd7a"
13
+ title:
14
+ font: DejaVu-Sans-Book
15
+ size: 33
16
+ color: Black
17
+ background: White
18
+ highlight:
19
+ background: LightGoldenRod
20
+ contact:
21
+ background: White
22
+ timestamp:
23
+ font: DejaVu-Sans-Book
24
+ size: 14
25
+ color: White
26
+ background: "#000000aa"
27
+ signature:
28
+ font: DejaVu-Sans-Book
29
+ size: 10
30
+ color: Black
31
+ background: SlateGray
32
+ lowlevel:
33
+ blank_evasion: true
34
+ blank_threshold: 0.10
35
+ blank_alternatives: [ -5, 5, -10, 10, -30, 30]
@@ -0,0 +1,25 @@
1
+ #
2
+ # FFmpeg Abstraction
3
+ #
4
+
5
+ require 'command'
6
+ require 'capturer'
7
+
8
+ module VCSRuby
9
+ class FFmpeg < Capturer
10
+ def initialize video
11
+ @video = video
12
+ @command = Command.new :ffmpeg, 'ffmpeg'
13
+ end
14
+
15
+ def name
16
+ :ffmpeg
17
+ end
18
+
19
+ def length
20
+ info = @command.execute("-i #{@video} -dframes 0 -vframes 0 /dev/null", "2>&1")
21
+ match = /Duration: ([\d|:|.]*)/.match(info)
22
+ return TimeIndex.new match[1]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # Font helper
3
+ #
4
+
5
+ require 'mini_magick'
6
+
7
+ module VCSRuby
8
+ class Font
9
+ attr_reader :name, :path, :size
10
+
11
+ def initialize name, size
12
+ @name = name
13
+ @path = find_path
14
+ @size = size
15
+ end
16
+
17
+ def find_path
18
+ '/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf'
19
+ end
20
+
21
+ def line_height
22
+ MiniMagick::Tool::Convert.new do |convert|
23
+ convert.font path
24
+ convert.pointsize size
25
+ convert << 'label:F'
26
+ convert.format '%h'
27
+ convert << 'info:'
28
+ end.to_i
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,144 @@
1
+ #
2
+ # FFmpeg Abstraction
3
+ #
4
+
5
+ require 'capturer'
6
+ require 'command'
7
+ require 'time_index'
8
+
9
+ module VCSRuby
10
+ class LibAV < Capturer
11
+
12
+ CODEC = 2
13
+ DIMENSION = 4
14
+ FPS = 6
15
+
16
+ def initialize video
17
+ @video = video
18
+ @avconv = Command.new :libav, 'avconv'
19
+ @avprobe = Command.new :libav, 'avprobe'
20
+ detect_version
21
+ end
22
+
23
+ def name
24
+ :libav
25
+ end
26
+
27
+ def available?
28
+ @avconv.available && @avprobe.available
29
+ end
30
+
31
+ def detect_version
32
+ info = @avconv.execute("-version")
33
+ match = /avconv ([\d|.|-|:]*)/.match(info)
34
+ @version = match[1]
35
+ end
36
+
37
+ def length
38
+ load_probe
39
+ match = /Duration: ([\d|:|.]*)/.match(@cache)
40
+ return TimeIndex.new match[1]
41
+ end
42
+
43
+ def width
44
+ load_probe
45
+ @width
46
+ end
47
+
48
+ def height
49
+ load_probe
50
+ @height
51
+ end
52
+
53
+ def par
54
+ load_probe
55
+ @par
56
+ end
57
+
58
+ def dar
59
+ load_probe
60
+ @dar
61
+ end
62
+
63
+ def fps
64
+ load_probe
65
+ @fps
66
+ end
67
+
68
+ def video_codec
69
+ load_probe
70
+
71
+ @video_codec
72
+ end
73
+
74
+ def audio_codec
75
+ load_probe
76
+
77
+ @audio_codec
78
+ end
79
+
80
+ def grab time, image_path
81
+ @avconv.execute "-y -ss #{time.total_seconds} -i '#{@video}' -an -dframes 1 -vframes 1 -vcodec png -f rawvideo '#{image_path}'"
82
+ end
83
+
84
+ def to_s
85
+ "LibAV #{@version}"
86
+ end
87
+
88
+ private
89
+ def load_probe
90
+ return if @cache
91
+
92
+ @cache = @avprobe.execute("'#{@video}'", "2>&1")
93
+ puts @cache if Tools.verbose?
94
+
95
+ parse_video_streams
96
+ parse_audio_streams
97
+ end
98
+
99
+ def parse_video_streams
100
+ video_stream = split_stream_line(is_stream?(@cache, /Video/).first)
101
+
102
+ dimensions = /(\d*)x(\d*) \[PAR (\d*:\d*) DAR (\d*:\d*)\]/.match(video_stream[DIMENSION])
103
+
104
+ if dimensions
105
+ @par = dimensions[3]
106
+ @dar = dimensions[4]
107
+ else
108
+ dimensions = /(\d*)x(\d*)/.match(video_stream[DIMENSION])
109
+ end
110
+
111
+ if dimensions
112
+ @width = dimensions[1].to_i
113
+ @height = dimensions[2].to_i
114
+ end
115
+
116
+ fps = /([\d|.]+) fps/.match(video_stream[FPS])
117
+ @fps = fps ? fps[1].to_f : 0.0
118
+
119
+ @video_codec = video_stream[CODEC]
120
+ end
121
+
122
+ def parse_audio_streams
123
+ audio_stream = split_stream_line(is_stream?(@cache, /Audio/).first)
124
+
125
+ @audio_codec = audio_stream[CODEC]
126
+ end
127
+
128
+ def is_stream? probe, regex
129
+ streams(probe).select{ |s| s =~ regex }
130
+ end
131
+
132
+ def streams probe
133
+ @cache.split(/\r?\n/).map(&:strip).select{|l| l.start_with? 'Stream' }
134
+ end
135
+
136
+ def split_stream_line line
137
+ parts = line.split(',')
138
+ stream = parts.shift
139
+ result = stream.split(':')
140
+ result += parts
141
+ return result.map(&:strip)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,19 @@
1
+ #
2
+ # MPlayer Abstraction
3
+ #
4
+
5
+ require 'command'
6
+ require 'capturer'
7
+
8
+ module VCSRuby
9
+ class MPlayer < Capturer
10
+ def initialize video
11
+ @video = video
12
+ @command = Command.new :mplayer, 'mplayer'
13
+ end
14
+
15
+ def name
16
+ :mplayer
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,105 @@
1
+ #
2
+ # Thumbnails from video
3
+ #
4
+
5
+ require 'mini_magick'
6
+
7
+ module VCSRuby
8
+ class Thumbnail
9
+ attr_accessor :width, :height, :aspect
10
+ attr_accessor :image_path
11
+ attr_accessor :time
12
+
13
+ def initialize capper, video, configuration
14
+ @capper = capper
15
+ @video = video
16
+ @configuration = configuration
17
+
18
+ @filters = [method(:resize_filter), method(:timestamp_filter), method(:softshadow_filter)]
19
+ end
20
+
21
+ def capture
22
+ @capper.grab @time, @image_path
23
+ end
24
+
25
+ def capture_and_evade interval
26
+ times = [TimeIndex.new] + @configuration.blank_alternatives
27
+ times.select! { |t| (t < interval / 2) and (t > interval / -2) }
28
+ times.map! { |t| @time + t }
29
+
30
+ times.each do |time|
31
+ @time = time
32
+ capture
33
+ break unless blank?
34
+ puts "Blank frame detected. => #{@time}" unless Tools::quiet?
35
+ puts "Giving up!" if time == times.last && !Tools::quiet?
36
+ end
37
+ end
38
+
39
+ def blank?
40
+ image = MiniMagick::Image.open @image_path
41
+ image.colorspace 'Gray'
42
+ mean = image['%[fx:image.mean]'].to_f
43
+ return mean < @configuration.blank_threshold
44
+ end
45
+
46
+ def apply_filters
47
+ MiniMagick::Tool::Convert.new do |convert|
48
+ convert.background 'Transparent'
49
+ convert.fill 'Transparent'
50
+ convert << @image_path
51
+ @filters.each do |filter|
52
+ filter.call(convert)
53
+ end
54
+ convert << @image_path
55
+ end
56
+ end
57
+
58
+ private
59
+ def resize_filter convert
60
+ convert.resize "#{width}x#{height}!"
61
+ end
62
+
63
+ def timestamp_filter convert
64
+ convert.stack do |box|
65
+ box.box @configuration.timestamp_background
66
+ box.fill @configuration.timestamp_color
67
+ box.pointsize @configuration.timestamp_font.size
68
+ box.gravity 'SouthEast'
69
+ box.font @configuration.timestamp_font.path
70
+ box.annotate('+10+10', " #{@time.to_timestamp} ")
71
+ end
72
+ convert.flatten
73
+ convert.gravity 'None'
74
+ end
75
+
76
+ def photoframe_filter convert
77
+ convert.bordercolor 'White'
78
+ convert.border 3
79
+ convert.bordercolor 'Grey60'
80
+ convert.border 1
81
+ end
82
+
83
+ def softshadow_filter convert
84
+ convert.stack do |box|
85
+ box.background 'Black'
86
+ box.clone.+
87
+ box.shadow '50x2+4+4'
88
+ box.background 'None'
89
+ end
90
+ convert.swap.+
91
+ convert.flatten
92
+ convert.trim
93
+ convert.repage.+
94
+ end
95
+
96
+ def polaroid_filter
97
+ end
98
+
99
+ def random_rotation_filter
100
+ end
101
+
102
+ def film_filter
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,121 @@
1
+ #
2
+ # Time interval
3
+ #
4
+
5
+ module VCSRuby
6
+ class TimeIndex
7
+ include Comparable
8
+ attr_reader :total_seconds
9
+
10
+ def initialize time_index = ''
11
+ if time_index.instance_of? Float or time_index.instance_of? Fixnum
12
+ @total_seconds = time_index
13
+ else
14
+ @total_seconds = 0.0
15
+ @to_parse = time_index.strip
16
+
17
+ unless @to_parse.empty?
18
+ try_parse_ffmpeg_index
19
+ try_parse_vcs_index
20
+ try_parse_as_number
21
+ end
22
+ end
23
+ end
24
+
25
+ def try_parse_ffmpeg_index
26
+ parts = @to_parse.split(':')
27
+ if parts.count == 3
28
+ @total_seconds += parts[0].to_i * 60 * 60
29
+ @total_seconds += parts[1].to_i * 60
30
+ @total_seconds += parts[2].to_f
31
+ end
32
+ end
33
+
34
+ def try_parse_vcs_index
35
+ if @to_parse =~ /\d*m|\d*h|\d*s/
36
+ parts = @to_parse.split(/(\d*h)|(\d*m)|(\d*s)/).select{|e| !e.empty?}
37
+ parts.each do |part|
38
+ add_vcs_part part
39
+ end
40
+ end
41
+ end
42
+
43
+ def add_vcs_part part
44
+ return @total_seconds += part.to_i * 60 * 60 if part.end_with? 'h'
45
+ return @total_seconds += part.to_i * 60 if part.end_with? 'm'
46
+ @total_seconds += part.to_i
47
+ end
48
+
49
+ def try_parse_as_number
50
+ temp = @to_parse.to_i
51
+ if temp.to_s == @to_parse
52
+ @total_seconds += temp
53
+ end
54
+ end
55
+
56
+ def total_seconds
57
+ @total_seconds
58
+ end
59
+
60
+ def hours
61
+ (@total_seconds.abs / 3600).to_i
62
+ end
63
+
64
+ def minutes
65
+ ((@total_seconds.abs / 60) % 60).to_i
66
+ end
67
+
68
+ def seconds
69
+ @total_seconds.abs % 60
70
+ end
71
+
72
+ def + operand
73
+ if operand.instance_of? Fixnum
74
+ TimeIndex.new @total_seconds + operand
75
+ else
76
+ TimeIndex.new @total_seconds + operand.total_seconds
77
+ end
78
+ end
79
+
80
+ def - operand
81
+ if operand.instance_of? Fixnum
82
+ TimeIndex.new @total_seconds - operand
83
+ else
84
+ TimeIndex.new @total_seconds - operand.total_seconds
85
+ end
86
+ end
87
+
88
+ def * operand
89
+ TimeIndex.new total_seconds * operand
90
+ end
91
+
92
+ def / operand
93
+ if operand.instance_of? Fixnum
94
+ TimeIndex.new @total_seconds / operand
95
+ else
96
+ @total_seconds / operand.total_seconds
97
+ end
98
+ end
99
+
100
+ def <=> operand
101
+ @total_seconds <=> operand.total_seconds
102
+ end
103
+
104
+ def sign
105
+ return '-' if @total_seconds < 0
106
+ ''
107
+ end
108
+
109
+ def to_s
110
+ "#{sign}#{hours}h#{"%02d" % minutes}m#{"%02d" % seconds}s"
111
+ end
112
+
113
+ def to_timestamp
114
+ if hours == 0
115
+ "#{sign}#{"%02d" % minutes}:#{"%02d" % seconds}"
116
+ else
117
+ "#{sign}#{hours}:#{"%02d" % minutes}:#{"%02d" % seconds}"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,67 @@
1
+ #
2
+ # Dependencies
3
+ #
4
+
5
+ module VCSRuby
6
+ class Tools
7
+ def self.windows?
8
+ false
9
+ end
10
+
11
+ def self.verbose= verbose
12
+ @verbose = verbose
13
+ @quiet = false if @verbose
14
+ end
15
+
16
+ def self.verbose?
17
+ @verbose
18
+ end
19
+
20
+ def self.quiet= quiet
21
+ @quiet = quiet
22
+ @verbose = false if @quiet
23
+ end
24
+
25
+ def self.quiet?
26
+ @quiet
27
+ end
28
+
29
+ def self.list_arguments arguments
30
+ arguments.map{ |argument| argument.to_s }.join(', ')
31
+ end
32
+
33
+ def self.print_help optparse
34
+ puts optparse.summarize
35
+ exit 0
36
+ end
37
+
38
+ def self.contact_sheet_with_options video, options
39
+ sheet = VCSRuby::ContactSheet.new video, options[:capturer]
40
+ sheet.format = options[:format] if options[:format]
41
+ sheet.title = options[:title] if options[:title]
42
+ sheet.signature = options[:signature] if options[:signature]
43
+ sheet.signature = nil if options[:no_signature]
44
+
45
+ if options[:rows] || options[:columns] || options[:interval]
46
+ sheet.initialize_geometry(options[:rows], options[:columns], options[:interval])
47
+ end
48
+
49
+ sheet.thumbnail_width = options[:width] if options[:width]
50
+ sheet.thumbnail_height = options[:height] if options[:height]
51
+ sheet.from = options[:from] if options[:from]
52
+ sheet.to = options[:to] if options[:to]
53
+ sheet.highlight = options[:highlight] if options[:highlight]
54
+
55
+ return sheet
56
+ end
57
+
58
+ def self.to_human_size size
59
+ powers = { 'B' => 1 << 10, 'KiB' => 1 << 20, 'MiB' => 1 << 30, 'GiB' => 1 << 40, 'TiB' => 1 << 50 }
60
+ powers.each_pair do |prefix, power|
61
+ if size < power
62
+ return format('%.2f',size.to_f / (power >> 10)) + ' ' + prefix
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ #
2
+ # Video Contact Sheet Ruby
3
+ #
4
+
5
+ require 'command'
6
+ require 'configuration'
7
+ require 'contact_sheet'
8
+ require 'ffmpeg'
9
+ require 'libav'
10
+ require 'mplayer'
11
+ require 'thumbnail'
12
+ require 'time_index'
13
+ require 'tools'
14
+ require 'version'
@@ -0,0 +1 @@
1
+ 0.8.1
@@ -0,0 +1,25 @@
1
+ #
2
+ # Version of vcs.rb
3
+ #
4
+
5
+ module VCSRuby
6
+ def self.version_path
7
+ File.expand_path("version.info", File.dirname(__FILE__))
8
+ end
9
+
10
+ def self.read_version
11
+ File.open(version_path, &:readline)
12
+ end
13
+
14
+ def self.update_version
15
+ parts = File.open(version_path, &:readline).split('.').map(&:strip)
16
+ parts[2] = (parts[2].to_i + 1).to_s
17
+ File.open(version_path, 'w') {|f| f.write(parts.join('.')) }
18
+
19
+ $vcs_ruby_version
20
+ end
21
+ end
22
+
23
+ $vcs_ruby_version = Gem::Version.new(VCSRuby::read_version)
24
+ $vcs_ruby_name = 'Video Contact Sheet Ruby'
25
+ $vcs_ruby_short = 'vcr.rb'
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vcs_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Thomas Bruderer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-04-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minimagick
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Creates a contact sheet of a video, usable as library or as a script.
31
+ Based on VCS *NIX
32
+ email: apophis@apophis.ch
33
+ executables:
34
+ - vcs.rb
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - lib/time_index.rb
39
+ - lib/font.rb
40
+ - lib/tools.rb
41
+ - lib/mplayer.rb
42
+ - lib/thumbnail.rb
43
+ - lib/contact_sheet.rb
44
+ - lib/ffmpeg.rb
45
+ - lib/version.rb
46
+ - lib/libav.rb
47
+ - lib/capturer.rb
48
+ - lib/version.info
49
+ - lib/vcs.rb
50
+ - lib/command.rb
51
+ - lib/configuration.rb
52
+ - lib/defaults.yml
53
+ - bin/vcs.rb
54
+ homepage: https://github.com/FreeApophis/vcs.rb
55
+ licenses:
56
+ - GPL3
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: 1.8.6
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements:
74
+ - libav or ffmpeg or mplayer
75
+ rubyforge_project:
76
+ rubygems_version: 1.8.23
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Generates contact sheets of videos
80
+ test_files: []