vcs_ruby 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/configuration.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  #
4
4
 
5
5
  require 'font'
6
-
6
+ require 'singleton'
7
7
 
8
8
  class ::Hash
9
9
  def deep_merge(second)
@@ -14,22 +14,21 @@ end
14
14
 
15
15
  module VCSRuby
16
16
  class Configuration
17
- attr_accessor :capturer
17
+ include Singleton
18
+
18
19
  attr_reader :header_font, :title_font, :timestamp_font, :signature_font
20
+ attr_writer :verbose, :quiet, :capturer
19
21
 
20
- def initialize profile
22
+ def initialize
21
23
  default_config_file = File.expand_path("defaults.yml", File.dirname(__FILE__))
22
24
  @config = ::YAML::load_file(default_config_file)
23
25
 
24
26
  local_config_files = ['~/.vcs.rb.yml']
25
27
  local_config_files.select{ |f| File.exists?(f) }.each do |local_config_file|
26
- puts "Local configuration file loaded: #{local_config_file}" if Tools.verbose?
27
28
  local_config = YAML::load_file(local_config_file)
28
29
  @config = @config.deep_merge(local_config)
29
30
  end
30
31
 
31
- load_profile profile if profile
32
-
33
32
  @header_font = Font.new @config['style']['header']['font'], @config['style']['header']['size']
34
33
  @title_font = Font.new @config['style']['title']['font'], @config['style']['title']['size']
35
34
  @timestamp_font = Font.new @config['style']['timestamp']['font'], @config['style']['timestamp']['size']
@@ -42,7 +41,7 @@ module VCSRuby
42
41
  found = false
43
42
  profiles.each do |profile|
44
43
  if File.exists?(profile)
45
- puts "Profile loaded: #{profile}" if Tools.verbose?
44
+ puts "Profile loaded: #{profile}" if verbose?
46
45
  config = YAML::load_file(profile)
47
46
  @config = @config.deep_merge(config)
48
47
  found = true
@@ -52,6 +51,18 @@ module VCSRuby
52
51
  raise "No profile '#{profile}' found" unless found
53
52
  end
54
53
 
54
+ def verbose?
55
+ @verbose
56
+ end
57
+
58
+ def quiet?
59
+ @quiet
60
+ end
61
+
62
+ def capturer
63
+ @capturer || :any
64
+ end
65
+
55
66
  def rows
56
67
  @config['main']['rows'] ? @config['main']['rows'].to_i : nil
57
68
  end
data/lib/contact_sheet.rb CHANGED
@@ -10,49 +10,45 @@ require 'vcs'
10
10
 
11
11
  module VCSRuby
12
12
  class ContactSheet
13
- attr_accessor :capturer, :format, :signature, :title, :highlight
13
+ attr_accessor :signature, :title, :highlight
14
14
  attr_accessor :softshadow, :timestamp, :polaroid
15
15
  attr_reader :thumbnail_width, :thumbnail_height
16
16
  attr_reader :length, :from, :to
17
17
 
18
- def initialize video, profile = nil
19
- @capturer = :any
20
- @configuration = Configuration.new profile
18
+ def initialize video, capturer
19
+ @video = video
20
+ @capturer = capturer
21
21
  @signature = "Created by Video Contact Sheet Ruby"
22
- initialize_capturers video
23
- initialize_filename(File.basename(@video, '.*'))
24
- puts "Processing #{File.basename(video)}..." unless Tools.quiet?
22
+ initialize_filename
23
+
24
+ if Configuration.instance.verbose?
25
+ puts "Processing #{File.basename(video.full_path)}..."
26
+ end
27
+
28
+ return unless @video.valid?
29
+
25
30
  detect_video_properties
26
31
 
27
- @thumbnails = []
28
- @filters = []
32
+ @from = TimeIndex.new 0
33
+ @to = @video.info.duration
29
34
 
30
- @timestamp = @configuration.timestamp
31
- @softshadow = @configuration.softshadow
32
- @polaroid = @configuration.polaroid
35
+ @timestamp = Configuration.instance.timestamp
36
+ @softshadow = Configuration.instance.softshadow
37
+ @polaroid = Configuration.instance.polaroid
33
38
 
34
39
  @tempdir = Dir.mktmpdir
35
40
 
36
41
  ObjectSpace.define_finalizer(self, self.class.finalize(@tempdir) )
42
+ initialize_geometry(Configuration.instance.rows, Configuration.instance.columns, Configuration.instance.interval)
37
43
 
38
- initialize_geometry(@configuration.rows, @configuration.columns, @configuration.interval)
39
44
  end
40
45
 
41
- def initialize_filename filename
42
- @out_path = File.dirname(filename)
43
- @out_filename = File.basename(filename,'.*')
44
- ext = File.extname(filename).gsub('.', '')
45
- if ['png', 'jpg', 'jpeg', 'tiff'].include?(ext)
46
- @format ||= ext.to_sym
47
- end
48
- end
46
+ def initialize_filename override = nil
47
+ @out_path = File.dirname(@video.full_path)
48
+ @out_filename = File.basename(override || @video.full_path,'.*')
49
49
 
50
- def filename
51
- "#{@out_filename}.#{@format ? @format.to_s : 'png'}"
52
- end
53
-
54
- def full_path
55
- File.join(@out_path, filename)
50
+ extension = override ? File.extname(override) : ''
51
+ @format = extension.length > 0 ? extension : '.png'
56
52
  end
57
53
 
58
54
  def initialize_geometry(rows, columns, interval)
@@ -62,6 +58,14 @@ module VCSRuby
62
58
  @interval = interval
63
59
  end
64
60
 
61
+ def filename
62
+ "#{@out_filename}#{@format}"
63
+ end
64
+
65
+ def full_path
66
+ File.join(@out_path, filename)
67
+ end
68
+
65
69
  def rows
66
70
  @rows
67
71
  end
@@ -71,7 +75,7 @@ module VCSRuby
71
75
  end
72
76
 
73
77
  def interval
74
- @interval || (@to - @from) / (number_of_caps + 1)
78
+ @interval || (@to - @from) / (number_of_caps)
75
79
  end
76
80
 
77
81
  def number_of_caps
@@ -112,61 +116,37 @@ module VCSRuby
112
116
  end
113
117
  end
114
118
 
115
-
116
- def self.finalize(tempdir)
117
- proc do
118
- puts "Cleaning up..." unless Tools.quiet?
119
- FileUtils.rm_r tempdir
120
- end
121
- end
122
-
123
119
  def build
124
- selected_capturer.format = selected_capturer.available_formats.first
125
- initialize_filters
126
- initialize_thumbnails
127
- capture_thumbnails
120
+ if (@video.info.duration.total_seconds < 1.0)
121
+ puts "Video is shorter than 1 sec"
122
+ else
123
+ initialize_filters
124
+ initialize_thumbnails
125
+ capture_thumbnails
128
126
 
129
- puts "Composing standard contact sheet..." unless Tools.quiet?
130
- montage = splice_montage(montage_thumbs)
127
+ puts "Composing standard contact sheet..." unless Configuration.instance.quiet?
128
+ montage = splice_montage(montage_thumbs)
131
129
 
132
- image = MiniMagick::Image.open(montage)
130
+ image = MiniMagick::Image.open(montage)
133
131
 
134
- puts "Adding header and footer..." unless Tools.quiet?
135
- final = add_header_and_footer image
132
+ puts "Adding header and footer..." unless Configuration.instance.quiet?
133
+ final = add_header_and_footer image
136
134
 
137
- puts "Done. Output wrote to '#{filename}'" unless Tools.quiet?
138
- FileUtils.mv(final, full_path)
135
+ puts "Done. Output wrote to '#{filename}'" unless Configuration.instance.quiet?
136
+ FileUtils.mv(final, full_path)
137
+ end
139
138
  end
140
139
 
141
-
142
140
  private
143
- def selected_capturer
144
- result = nil
145
- if @capturer == nil || @capturer == :any
146
- result = available_capturers.first
147
- else
148
- result = available_capturers.select{ |c| c.name == @capturer }.first
141
+ def self.finalize(tempdir)
142
+ proc do
143
+ puts "Cleaning up..." unless Configuration.instance.quiet?
144
+ FileUtils.rm_r tempdir
149
145
  end
150
- raise "Selected Capturer (#{@capturer.to_s}) not available. Install one of these: #{@capturers.map{ |c| c.name }.join(', ')}" unless result
151
- return result
152
- end
153
-
154
- def initialize_capturers video
155
- @capturers = []
156
- @capturers << LibAV.new(video)
157
- @capturers << MPlayer.new(video)
158
- @capturers << FFmpeg.new(video)
159
-
160
- @video = video
161
-
162
- puts "Available capturers: #{available_capturers.map{ |c| c.to_s }.join(', ')}" if Tools.verbose?
163
- end
164
-
165
- def available_capturers
166
- @capturers.select{ |c| c.available? }
167
146
  end
168
147
 
169
148
  def initialize_filters
149
+ @filters = []
170
150
  @filters << :resize_filter
171
151
  @filters << :softshadow_filter if softshadow
172
152
  @filters << :timestamp_filter if timestamp
@@ -174,14 +154,14 @@ private
174
154
  end
175
155
 
176
156
  def initialize_thumbnails
177
- time = @from
157
+ @thumbnails = []
158
+ time = @from + (interval / 2)
178
159
  (1..number_of_caps).each do |i|
179
- thumb = Thumbnail.new selected_capturer, @video, @configuration
180
-
160
+ thumb = Frame.new @video, @capturer, time
161
+ time = time + interval
181
162
  thumb.width = thumbnail_width
182
163
  thumb.height = thumbnail_height
183
- thumb.time = (time += interval)
184
- thumb.image_path = File::join(@tempdir, "th#{"%03d" % i}.#{selected_capturer.format.to_s}")
164
+ thumb.filename = File::join(@tempdir, "th#{"%03d" % i}.#{@capturer.format_extension}")
185
165
  thumb.filters.push(*@filters)
186
166
 
187
167
  @thumbnails << thumb
@@ -189,11 +169,11 @@ private
189
169
  end
190
170
 
191
171
  def capture_thumbnails
192
- puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Tools.quiet?
172
+ puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Configuration.instance.quiet?
193
173
 
194
174
  @thumbnails.each_with_index do |thumbnail, i|
195
- puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Tools::quiet?
196
- if @configuration.blank_evasion?
175
+ puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Configuration.instance.quiet?
176
+ if Configuration.instance.blank_evasion?
197
177
  thumbnail.capture_and_evade interval
198
178
  else
199
179
  thumbnail.capture
@@ -208,25 +188,25 @@ private
208
188
  end
209
189
 
210
190
  def detect_length
211
- @length = selected_capturer.length
191
+ @length = @video.info.duration
212
192
 
213
193
  @from = TimeIndex.new 0.0
214
194
  @to = @length
215
195
  end
216
196
 
217
197
  def detect_dimensions
218
- @thumbnail_width = selected_capturer.width
219
- @thumbnail_height = selected_capturer.height
198
+ @thumbnail_width = @video.video.width
199
+ @thumbnail_height = @video.video.height
220
200
  end
221
201
 
222
202
  def montage_thumbs
223
203
  file_path = File::join(@tempdir, 'montage.png')
224
204
  MiniMagick::Tool::Montage.new do |montage|
225
- montage.background @configuration.contact_background
205
+ montage.background Configuration.instance.contact_background
226
206
  @thumbnails.each do |thumbnail|
227
- montage << thumbnail.image_path
207
+ montage << thumbnail.filename
228
208
  end
229
- montage.geometry "+#{@configuration.padding}+#{@configuration.padding}"
209
+ montage.geometry "+#{Configuration.instance.padding}+#{Configuration.instance.padding}"
230
210
  # rows or columns can be nil (auto fit)
231
211
  montage.tile "#{@columns}x#{@rows}"
232
212
  montage << file_path
@@ -236,18 +216,18 @@ private
236
216
 
237
217
  def splice_montage montage_path
238
218
  if softshadow
239
- left = @configuration.padding + 3
240
- top = @configuration.padding + 5
241
- bottom = right = @configuration.padding
219
+ left = Configuration.instance.padding + 3
220
+ top = Configuration.instance.padding + 5
221
+ bottom = right = Configuration.instance.padding
242
222
  else
243
- left = right = top = bottom = @configuration.padding
244
- end
223
+ left = right = top = bottom = Configuration.instance.padding
224
+ end
245
225
 
246
226
 
247
227
  file_path = File::join(@tempdir, 'spliced.png')
248
228
  MiniMagick::Tool::Convert.new do |convert|
249
229
  convert << montage_path
250
- convert.background @configuration.contact_background
230
+ convert.background Configuration.instance.contact_background
251
231
 
252
232
  convert.splice "#{left}x#{top}"
253
233
  convert.gravity 'SouthEast'
@@ -262,14 +242,14 @@ private
262
242
  file_path = File::join(@tempdir, 'title.png')
263
243
  MiniMagick::Tool::Convert.new do |convert|
264
244
  convert.stack do |ul|
265
- ul.size "#{montage.width}x#{@configuration.title_font.line_height}"
266
- ul.xc @configuration.title_background
267
- if @configuration.title_font.exists?
268
- ul.font @configuration.title_font.path
245
+ ul.size "#{montage.width}x#{Configuration.instance.title_font.line_height}"
246
+ ul.xc Configuration.instance.title_background
247
+ if Configuration.instance.title_font.exists?
248
+ ul.font Configuration.instance.title_font.path
269
249
  end
270
- ul.pointsize @configuration.title_font.size
271
- ul.background @configuration.title_background
272
- ul.fill @configuration.title_color
250
+ ul.pointsize Configuration.instance.title_font.size
251
+ ul.background Configuration.instance.title_background
252
+ ul.fill Configuration.instance.title_color
273
253
  ul.gravity 'Center'
274
254
  ul.annotate(0, @title)
275
255
  end
@@ -281,12 +261,11 @@ private
281
261
 
282
262
  def create_highlight montage
283
263
  puts "Generating highlight..."
284
- thumb = Thumbnail.new selected_capturer, @video, @configuration
264
+ thumb = Frame.new @video, @highlight
285
265
 
286
266
  thumb.width = thumbnail_width
287
267
  thumb.height = thumbnail_height
288
- thumb.time = @highlight
289
- thumb.image_path = File::join(@tempdir, "highlight_thumb.png")
268
+ thumb.filename = File::join(@tempdir, "highlight_thumb.png")
290
269
  thumb.capture
291
270
  thumb.apply_filters
292
271
 
@@ -294,9 +273,9 @@ private
294
273
  MiniMagick::Tool::Convert.new do |convert|
295
274
  convert.stack do |a|
296
275
  a.size "#{montage.width}x#{thumbnail_height+20}"
297
- a.xc @configuration.highlight_background
276
+ a.xc Configuration.instance.highlight_background
298
277
  a.gravity 'Center'
299
- a << thumb.image_path
278
+ a << thumb.filename
300
279
  a.composite
301
280
  end
302
281
  convert.stack do |a|
@@ -312,33 +291,33 @@ private
312
291
 
313
292
  def add_header_and_footer montage
314
293
  file_path = File::join(@tempdir, filename)
315
- header_height = @configuration.header_font.line_height * 3
316
- signature_height = @configuration.signature_font.line_height + 8
294
+ header_height = Configuration.instance.header_font.line_height * 3
295
+ signature_height = Configuration.instance.signature_font.line_height + 8
317
296
  MiniMagick::Tool::Convert.new do |convert|
318
297
  convert.stack do |a|
319
298
  a.size "#{montage.width - 18}x1"
320
- a.xc @configuration.header_background
299
+ a.xc Configuration.instance.header_background
321
300
  a.size.+
322
- if @configuration.header_font.exists?
323
- a.font @configuration.header_font.path
301
+ if Configuration.instance.header_font.exists?
302
+ a.font Configuration.instance.header_font.path
324
303
  end
325
- a.pointsize @configuration.header_font.size
326
- a.background @configuration.header_background
327
- a.fill @configuration.header_color
304
+ a.pointsize Configuration.instance.header_font.size
305
+ a.background Configuration.instance.header_background
306
+ a.fill Configuration.instance.header_color
328
307
  a.stack do |b|
329
308
  b.gravity 'West'
330
309
  b.stack do |c|
331
310
  c.label 'Filename: '
332
- if @configuration.header_font.exists?
333
- c.font @configuration.header_font.path
311
+ if Configuration.instance.header_font.exists?
312
+ c.font Configuration.instance.header_font.path
334
313
  end
335
- c.label File.basename(@video)
314
+ c.label File.basename(@video.full_path)
336
315
  c.append.+
337
316
  end
338
- if @configuration.header_font.exists?
339
- b.font @configuration.header_font.path
317
+ if Configuration.instance.header_font.exists?
318
+ b.font Configuration.instance.header_font.path
340
319
  end
341
- b.label "File size: #{Tools.to_human_size(File.size(@video))}"
320
+ b.label "File size: #{Tools.to_human_size(File.size(@video.full_path))}"
342
321
  b.label "Length: #{@length.to_timestamp}"
343
322
  b.append
344
323
  b.crop "#{montage.width}x#{header_height}+0+0"
@@ -347,11 +326,11 @@ private
347
326
  a.stack do |b|
348
327
  b.size "#{montage.width}x#{header_height}"
349
328
  b.gravity 'East'
350
- b.fill @configuration.header_color
329
+ b.fill Configuration.instance.header_color
351
330
  b.annotate '+0-1'
352
- b << "Dimensions: #{selected_capturer.width}x#{selected_capturer.height}\nFormat: #{selected_capturer.video_codec} / #{selected_capturer.audio_codec}\nFPS: #{"%.02f" % selected_capturer.fps}"
331
+ b << "Dimensions: #{@video.video.width}x#{@video.video.height}\nFormat: #{@video.video.codec(true)} / #{@video.audio.codec(true)}\nFPS: #{"%.02f" % @video.video.frame_rate.to_f}"
353
332
  end
354
- a.bordercolor @configuration.header_background
333
+ a.bordercolor Configuration.instance.header_background
355
334
  a.border 9
356
335
  end
357
336
  convert << create_title(montage) if @title
@@ -362,18 +341,18 @@ private
362
341
  convert.stack do |a|
363
342
  a.size "#{montage.width}x#{signature_height}"
364
343
  a.gravity 'Center'
365
- a.xc @configuration.signature_background
366
- if @configuration.signature_font.exists?
367
- a.font @configuration.signature_font.path
344
+ a.xc Configuration.instance.signature_background
345
+ if Configuration.instance.signature_font.exists?
346
+ a.font Configuration.instance.signature_font.path
368
347
  end
369
- a.pointsize @configuration.signature_font.size
370
- a.fill @configuration.signature_color
348
+ a.pointsize Configuration.instance.signature_font.size
349
+ a.fill Configuration.instance.signature_color
371
350
  a.annotate(0, @signature)
372
351
  end
373
352
  convert.append
374
353
  end
375
- if format == :jpg || format == :jpeg
376
- convert.quality(@configuration.quality)
354
+ if @format == :jpg || @format == :jpeg
355
+ convert.quality(Configuration.instance.quality)
377
356
  end
378
357
  convert << file_path
379
358
  end
data/lib/defaults.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  main:
2
2
  rows: 4
3
- columns: 4
3
+ columns: 4
4
4
  interval: ~
5
5
  padding: 2
6
6
  quality: 95
@@ -20,7 +20,7 @@ style:
20
20
  color: Black
21
21
  background: White
22
22
  highlight:
23
- background: LightGoldenRod
23
+ background: LightGoldenRod
24
24
  contact:
25
25
  background: White
26
26
  timestamp:
@@ -35,5 +35,5 @@ style:
35
35
  background: SlateGray
36
36
  lowlevel:
37
37
  blank_evasion: true
38
- blank_threshold: 0.10
39
- blank_alternatives: [ -5, 5, -10, 10, -30, 30]
38
+ blank_threshold: 0.08
39
+ blank_alternatives: [ -2, 2, -5, 5, -10, 10, -15, 15, -30, 30, 0]
data/lib/font.rb CHANGED
@@ -12,29 +12,29 @@ module VCSRuby
12
12
  attr_reader :name, :path, :size
13
13
 
14
14
  @@fonts = {}
15
-
15
+
16
16
  def initialize name, size
17
17
  @name = name
18
18
  @path = find_path
19
19
  @size = size
20
20
  end
21
-
21
+
22
22
  def exists?
23
23
  load_font_cache if @@fonts.length == 0
24
-
24
+
25
25
  !!font_by_name(@name)
26
26
  end
27
-
27
+
28
28
  def find_path
29
29
  load_font_cache if @@fonts.length == 0
30
-
30
+
31
31
  if exists?
32
32
  font_by_name(@name).glyphs
33
33
  else
34
34
  nil
35
35
  end
36
36
  end
37
-
37
+
38
38
  def font_by_name name
39
39
  if name =~ /\./
40
40
  key, font = @@fonts.select{ |key, f| f.glyphs =~ /#{name}\z/ }.first
@@ -43,26 +43,26 @@ module VCSRuby
43
43
  @@fonts[name]
44
44
  end
45
45
  end
46
-
46
+
47
47
  def load_font_cache
48
-
48
+
49
49
  fonts = MiniMagick::Tool::Identify.new(whiny: false) do |identify|
50
50
  identify.list 'font'
51
51
  end
52
52
 
53
53
  parse_fonts(fonts)
54
54
  end
55
-
55
+
56
56
  def parse_fonts(fonts)
57
57
  font = nil
58
58
  fonts.lines.each do |line|
59
59
  key, value = line.strip.split(':', 2).map(&:strip)
60
-
60
+
61
61
  next if [nil, 'Path'].include? key
62
62
 
63
63
  if key == 'Font'
64
- @@fonts[value] = font = IMFont.new(value)
65
- else
64
+ @@fonts[value] = font = IMFont.new(value)
65
+ else
66
66
  font.send("#{key}=", value)
67
67
  end
68
68
  end