vcs_ruby 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []