vcs_ruby 1.0.1 → 1.1.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.
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