lib/contact_sheet.rb in vcs_ruby-1.1.8 vs lib/contact_sheet.rb in vcs_ruby-1.1.9
- old
+ new
@@ -1,366 +1,380 @@
-#
-# Contact Sheet Composited from the Thumbnails
-#
-
-require 'fileutils'
-require 'tmpdir'
-require 'yaml'
-
-require 'vcs'
-
-module VCSRuby
- class ContactSheet
- attr_accessor :signature, :title, :highlight
- attr_accessor :softshadow, :timestamp, :polaroid
- attr_reader :thumbnail_width, :thumbnail_height, :format
- attr_reader :length, :from, :to
-
- def initialize video, capturer
- @video = video
- @capturer = capturer
- @signature = "Created by Video Contact Sheet Ruby"
- initialize_filename
-
- if Configuration.instance.verbose?
- puts "Processing #{File.basename(video.full_path)}..."
- end
-
- return unless @video.valid?
-
- detect_video_properties
-
- @from = TimeIndex.new 0
- @to = @video.info.duration
-
- @timestamp = Configuration.instance.timestamp
- @softshadow = Configuration.instance.softshadow
- @polaroid = Configuration.instance.polaroid
-
- @tempdir = Dir.mktmpdir
-
- ObjectSpace.define_finalizer(self, self.class.finalize(@tempdir) )
- initialize_geometry(Configuration.instance.rows, Configuration.instance.columns, Configuration.instance.interval)
-
- end
-
- def initialize_filename override = nil
- @out_path = File.dirname(@video.full_path)
- @out_filename = File.basename(override || @video.full_path,'.*')
-
- extension = override ? File.extname(override) : ''
- @format = extension.length > 0 ? extension : '.png'
- end
-
- def initialize_geometry(rows, columns, interval)
- @has_interval = !!interval
- @rows = rows
- @columns = columns
- @interval = interval
- end
-
- def filename
- "#{@out_filename}#{@format}"
- end
-
- def format= format
- @format = ".#{format.to_s}"
- end
-
- def full_path
- File.join(@out_path, filename)
- end
-
- def rows
- @rows
- end
-
- def columns
- @columns
- end
-
- def interval
- @interval || (@to - @from) / (number_of_caps)
- end
-
- def number_of_caps
- if @has_interval
- (@to - @from) / @interval
- else
- if @rows && @columns
- @rows * @columns
- else
- raise "you need at least 2 parameters from columns, rows and interval"
- end
- end
- end
-
- def thumbnail_width= width
- @thumbnail_height = (width.to_f / @thumbnail_width * thumbnail_height).to_i
- @thumbnail_width = width
- end
-
- def thumbnail_height= height
- @thumbnail_width = (height.to_f / @thumbnail_height * thumbnail_width).to_i
- @thumbnail_height = height
- end
-
- def from= time
- if (TimeIndex.new(0) < time) && (time < to) && (time < @length)
- @from = time
- else
- raise "Invalid From Time"
- end
- end
-
- def to= time
- if (TimeIndex.new(0) < time) && (from < time) && (time < @length)
- @to = time
- else
- raise "Invalid To Time"
- end
- end
-
- def build
- if (@video.info.duration.total_seconds < 1.0)
- puts "Video is shorter than 1 sec"
- else
- initialize_filters
- initialize_thumbnails
- capture_thumbnails
-
- puts "Composing standard contact sheet..." unless Configuration.instance.quiet?
- montage = splice_montage(montage_thumbs)
-
- image = MiniMagick::Image.open(montage)
-
- puts "Adding header and footer..." unless Configuration.instance.quiet?
- final = add_header_and_footer image
-
- puts "Done. Output wrote to '#{filename}'" unless Configuration.instance.quiet?
- FileUtils.mv(final, full_path)
- end
- end
-
-private
- def self.finalize(tempdir)
- proc do
- puts "Cleaning up..." unless Configuration.instance.quiet?
- FileUtils.rm_r tempdir
- end
- end
-
- def initialize_filters
- @filters = []
- @filters << :resize_filter
- @filters << :softshadow_filter if softshadow
- @filters << :timestamp_filter if timestamp
- @filters << :polaroid_filter if polaroid
- end
-
- def initialize_thumbnails
- @thumbnails = []
- time = @from + (interval / 2)
- (1..number_of_caps).each do |i|
- thumb = Frame.new @video, @capturer, time
- time = time + interval
- thumb.width = thumbnail_width
- thumb.height = thumbnail_height
- thumb.filename = File::join(@tempdir, "th#{"%03d" % i}.#{@capturer.format_extension}")
- thumb.filters.push(*@filters)
-
- @thumbnails << thumb
- end
- end
-
- def capture_thumbnails
- puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Configuration.instance.quiet?
-
- @thumbnails.each_with_index do |thumbnail, i|
- puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Configuration.instance.quiet?
- if Configuration.instance.blank_evasion?
- thumbnail.capture_and_evade interval
- else
- thumbnail.capture
- end
- thumbnail.apply_filters
- end
- end
-
- def detect_video_properties
- detect_length
- detect_dimensions
- end
-
- def detect_length
- @length = @video.info.duration
-
- @from = TimeIndex.new 0.0
- @to = @length
- end
-
- def detect_dimensions
- @thumbnail_width = @video.video.width
- @thumbnail_height = @video.video.height
- end
-
- def montage_thumbs
- file_path = File::join(@tempdir, 'montage.png')
- MiniMagick::Tool::Montage.new do |montage|
- montage.background Configuration.instance.contact_background
- @thumbnails.each do |thumbnail|
- montage << thumbnail.filename
- end
- montage.geometry "+#{Configuration.instance.padding}+#{Configuration.instance.padding}"
- # rows or columns can be nil (auto fit)
- montage.tile "#{@columns}x#{@rows}"
- montage << file_path
- end
- return file_path
- end
-
- def splice_montage montage_path
- if softshadow
- left = Configuration.instance.padding + 3
- top = Configuration.instance.padding + 5
- bottom = right = Configuration.instance.padding
- else
- left = right = top = bottom = Configuration.instance.padding
- end
-
-
- file_path = File::join(@tempdir, 'spliced.png')
- MiniMagick::Tool::Convert.new do |convert|
- convert << montage_path
- convert.background Configuration.instance.contact_background
-
- convert.splice "#{left}x#{top}"
- convert.gravity 'SouthEast'
- convert.splice "#{right}x#{bottom}"
-
- convert << file_path
- end
- file_path
- end
-
- def create_title montage
- file_path = File::join(@tempdir, 'title.png')
- MiniMagick::Tool::Convert.new do |convert|
- convert.stack do |ul|
- ul.size "#{montage.width}x#{Configuration.instance.title_font.line_height}"
- ul.xc Configuration.instance.title_background
- if Configuration.instance.title_font.exists?
- ul.font Configuration.instance.title_font.path
- end
- ul.pointsize Configuration.instance.title_font.size
- ul.background Configuration.instance.title_background
- ul.fill Configuration.instance.title_color
- ul.gravity 'Center'
- ul.annotate(0, @title)
- end
- convert.flatten
- convert << file_path
- end
- return file_path
- end
-
- def create_highlight montage
- puts "Generating highlight..."
- thumb = Frame.new @video, @highlight
-
- thumb.width = thumbnail_width
- thumb.height = thumbnail_height
- thumb.filename = File::join(@tempdir, "highlight_thumb.png")
- thumb.capture
- thumb.apply_filters
-
- file_path = File::join(@tempdir, "highlight.png")
- MiniMagick::Tool::Convert.new do |convert|
- convert.stack do |a|
- a.size "#{montage.width}x#{thumbnail_height+20}"
- a.xc Configuration.instance.highlight_background
- a.gravity 'Center'
- a << thumb.filename
- a.composite
- end
- convert.stack do |a|
- a.size "#{montage.width}x1"
- a.xc 'Black'
- end
- convert.append
- convert << file_path
- end
-
- file_path
- end
-
- def add_header_and_footer montage
- file_path = File::join(@tempdir, filename)
- header_height = Configuration.instance.header_font.line_height * 3
- signature_height = Configuration.instance.signature_font.line_height + 8
- MiniMagick::Tool::Convert.new do |convert|
- convert.stack do |a|
- a.size "#{montage.width - 18}x1"
- a.xc Configuration.instance.header_background
- a.size.+
- if Configuration.instance.header_font.exists?
- a.font Configuration.instance.header_font.path
- end
- a.pointsize Configuration.instance.header_font.size
- a.background Configuration.instance.header_background
- a.fill Configuration.instance.header_color
- a.stack do |b|
- b.gravity 'West'
- b.stack do |c|
- c.label 'Filename: '
- if Configuration.instance.header_font.exists?
- c.font Configuration.instance.header_font.path
- end
- c.label File.basename(@video.full_path)
- c.append.+
- end
- if Configuration.instance.header_font.exists?
- b.font Configuration.instance.header_font.path
- end
- b.label "File size: #{Tools.to_human_size(File.size(@video.full_path))}"
- b.label "Length: #{@length.to_timestamp}"
- b.append
- b.crop "#{montage.width}x#{header_height}+0+0"
- end
- a.append
- a.stack do |b|
- b.size "#{montage.width}x#{header_height}"
- b.gravity 'East'
- b.fill Configuration.instance.header_color
- b.annotate '+0-1'
- b << "Dimensions: #{@video.video.width}x#{@video.video.height}\nFormat: #{@video.video.codec(true)} / #{@video.audio ? @video.audio.codec(true) : 'no audio'}\nFPS: #{"%.02f" % @video.video.frame_rate.to_f}"
- end
- a.bordercolor Configuration.instance.header_background
- a.border 9
- end
- convert << create_title(montage) if @title
- convert << create_highlight(montage) if @highlight
- convert << montage.path
- convert.append
- if @signature && @signature.length > 0
- convert.stack do |a|
- a.size "#{montage.width}x#{signature_height}"
- a.gravity 'Center'
- a.xc Configuration.instance.signature_background
- if Configuration.instance.signature_font.exists?
- a.font Configuration.instance.signature_font.path
- end
- a.pointsize Configuration.instance.signature_font.size
- a.fill Configuration.instance.signature_color
- a.annotate(0, @signature)
- end
- convert.append
- end
- if @format == :jpg || @format == :jpeg
- convert.quality(Configuration.instance.quality)
- end
- convert << file_path
- end
- file_path
- end
- end
-end
+#
+# Contact Sheet Composited from the Thumbnails
+#
+
+require 'fileutils'
+require 'tmpdir'
+require 'yaml'
+
+require 'vcs'
+
+module VCSRuby
+ class ContactSheet
+ attr_accessor :signature, :title, :highlight
+ attr_accessor :softshadow, :timestamp, :polaroid
+ attr_reader :thumbnail_width, :thumbnail_height, :aspect_ratio, :format
+ attr_reader :length, :from, :to
+
+ def initialize video, capturer
+ @video = video
+ @capturer = capturer
+ @signature = "Created by Video Contact Sheet Ruby"
+ initialize_filename
+
+ if Configuration.instance.verbose?
+ puts "Processing #{File.basename(video.full_path)}..."
+ end
+
+ return unless @video.valid?
+
+ detect_video_properties
+
+ @from = TimeIndex.new 0
+ @to = @video.info.duration
+
+ @timestamp = Configuration.instance.timestamp
+ @softshadow = Configuration.instance.softshadow
+ @polaroid = Configuration.instance.polaroid
+
+ @tempdir = Dir.mktmpdir
+
+ ObjectSpace.define_finalizer(self, self.class.finalize(@tempdir) )
+ initialize_geometry(Configuration.instance.rows, Configuration.instance.columns, Configuration.instance.interval)
+
+ end
+
+ def initialize_filename override = nil
+ @out_path = File.dirname(@video.full_path)
+ @out_filename = File.basename(override || @video.full_path,'.*')
+
+ extension = override ? File.extname(override) : ''
+ @format = extension.length > 0 ? extension : '.png'
+ end
+
+ def initialize_geometry(rows, columns, interval)
+ @has_interval = !!interval
+ @rows = rows
+ @columns = columns
+ @interval = interval
+ end
+
+ def filename
+ "#{@out_filename}#{@format}"
+ end
+
+ def format= format
+ @format = ".#{format.to_s}"
+ end
+
+ def full_path
+ File.join(@out_path, filename)
+ end
+
+ def rows
+ @rows
+ end
+
+ def columns
+ @columns
+ end
+
+ def interval
+ @interval || (@to - @from) / (number_of_caps)
+ end
+
+ def number_of_caps
+ if @has_interval
+ (@to - @from) / @interval
+ else
+ if @rows && @columns
+ @rows * @columns
+ else
+ raise "you need at least 2 parameters from columns, rows and interval"
+ end
+ end
+ end
+
+ def thumbnail_width= width
+ @thumbnail_height = (width.to_f / @thumbnail_width * thumbnail_height).to_i
+ @thumbnail_width = width
+ end
+
+ def thumbnail_height= height
+ @thumbnail_width = (height.to_f / @thumbnail_height * thumbnail_width).to_i
+ @thumbnail_height = height
+ end
+
+ def aspect_ratio= aspect_ratio
+ @thumbnail_width = (@thumbnail_height * aspect_ratio).to_i
+ @aspect_ratio = aspect_ratio
+ end
+
+ def from= time
+ if (TimeIndex.new(0) < time) && (time < to) && (time < @length)
+ @from = time
+ else
+ raise "Invalid From Time"
+ end
+ end
+
+ def to= time
+ if (TimeIndex.new(0) < time) && (from < time) && (time < @length)
+ @to = time
+ else
+ raise "Invalid To Time"
+ end
+ end
+
+ def build
+ if (@video.info.duration.total_seconds < 1.0)
+ puts "Video is shorter than 1 sec"
+ else
+ initialize_filters
+ initialize_thumbnails
+ capture_thumbnails
+
+ puts "Composing standard contact sheet..." unless Configuration.instance.quiet?
+ montage = splice_montage(montage_thumbs)
+
+ image = MiniMagick::Image.open(montage)
+
+ puts "Adding header and footer..." unless Configuration.instance.quiet?
+ final = add_header_and_footer image
+
+ puts "Done. Output wrote to '#{filename}'" unless Configuration.instance.quiet?
+ FileUtils.mv(final, full_path)
+ end
+ end
+
+private
+ def self.finalize(tempdir)
+ proc do
+ puts "Cleaning up..." unless Configuration.instance.quiet?
+ FileUtils.rm_r tempdir
+ end
+ end
+
+ def initialize_filters
+ @filters = []
+ @filters << :resize_filter
+ @filters << :softshadow_filter if softshadow
+ @filters << :timestamp_filter if timestamp
+ @filters << :polaroid_filter if polaroid
+ end
+
+ def initialize_thumbnails
+ @thumbnails = []
+ time = @from + (interval / 2)
+ (1..number_of_caps).each do |i|
+ thumb = Frame.new @video, @capturer, time
+ time = time + interval
+ thumb.width = thumbnail_width
+ thumb.height = thumbnail_height
+ thumb.filename = File::join(@tempdir, "th#{"%03d" % i}.#{@capturer.format_extension}")
+ thumb.filters.push(*@filters)
+
+ @thumbnails << thumb
+ end
+ end
+
+ def capture_thumbnails
+ puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Configuration.instance.quiet?
+
+ @thumbnails.each_with_index do |thumbnail, i|
+ puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Configuration.instance.quiet?
+ if Configuration.instance.blank_evasion?
+ thumbnail.capture_and_evade interval
+ else
+ thumbnail.capture
+ end
+ thumbnail.apply_filters
+ end
+ end
+
+ def detect_video_properties
+ detect_length
+ detect_dimensions
+ end
+
+ def detect_length
+ @length = @video.info.duration
+
+ @from = TimeIndex.new 0.0
+ @to = @length
+ end
+
+ def detect_dimensions
+ @thumbnail_width = @video.video.width
+ @thumbnail_height = @video.video.height
+
+ @aspect_ratio = @video.video.aspect_ratio
+
+ if @aspect_ratio == 0
+ @aspect_ratio = Rational(@video.video.width, @video.video.height)
+ else
+ #recalculate width, for PAR 1:1 this should be the same as before
+ @thumbnail_width = (@aspect_ratio * @video.video.height).to_i
+ end
+ end
+
+ def montage_thumbs
+ file_path = File::join(@tempdir, 'montage.png')
+ MiniMagick::Tool::Montage.new do |montage|
+ montage.background Configuration.instance.contact_background
+ @thumbnails.each do |thumbnail|
+ montage << thumbnail.filename
+ end
+ montage.geometry "+#{Configuration.instance.padding}+#{Configuration.instance.padding}"
+ # rows or columns can be nil (auto fit)
+ montage.tile "#{@columns}x#{@rows}"
+ montage << file_path
+ end
+ return file_path
+ end
+
+ def splice_montage montage_path
+ if softshadow
+ left = Configuration.instance.padding + 3
+ top = Configuration.instance.padding + 5
+ bottom = right = Configuration.instance.padding
+ else
+ left = right = top = bottom = Configuration.instance.padding
+ end
+
+
+ file_path = File::join(@tempdir, 'spliced.png')
+ MiniMagick::Tool::Convert.new do |convert|
+ convert << montage_path
+ convert.background Configuration.instance.contact_background
+
+ convert.splice "#{left}x#{top}"
+ convert.gravity 'SouthEast'
+ convert.splice "#{right}x#{bottom}"
+
+ convert << file_path
+ end
+ file_path
+ end
+
+ def create_title montage
+ file_path = File::join(@tempdir, 'title.png')
+ MiniMagick::Tool::Convert.new do |convert|
+ convert.stack do |ul|
+ ul.size "#{montage.width}x#{Configuration.instance.title_font.line_height}"
+ ul.xc Configuration.instance.title_background
+ if Configuration.instance.title_font.exists?
+ ul.font Configuration.instance.title_font.path
+ end
+ ul.pointsize Configuration.instance.title_font.size
+ ul.background Configuration.instance.title_background
+ ul.fill Configuration.instance.title_color
+ ul.gravity 'Center'
+ ul.annotate(0, @title)
+ end
+ convert.flatten
+ convert << file_path
+ end
+ return file_path
+ end
+
+ def create_highlight montage
+ puts "Generating highlight..."
+ thumb = Frame.new @video, @highlight
+
+ thumb.width = thumbnail_width
+ thumb.height = thumbnail_height
+ thumb.filename = File::join(@tempdir, "highlight_thumb.png")
+ thumb.capture
+ thumb.apply_filters
+
+ file_path = File::join(@tempdir, "highlight.png")
+ MiniMagick::Tool::Convert.new do |convert|
+ convert.stack do |a|
+ a.size "#{montage.width}x#{thumbnail_height+20}"
+ a.xc Configuration.instance.highlight_background
+ a.gravity 'Center'
+ a << thumb.filename
+ a.composite
+ end
+ convert.stack do |a|
+ a.size "#{montage.width}x1"
+ a.xc 'Black'
+ end
+ convert.append
+ convert << file_path
+ end
+
+ file_path
+ end
+
+ def add_header_and_footer montage
+ file_path = File::join(@tempdir, filename)
+ header_height = Configuration.instance.header_font.line_height * 3
+ signature_height = Configuration.instance.signature_font.line_height + 8
+ MiniMagick::Tool::Convert.new do |convert|
+ convert.stack do |a|
+ a.size "#{montage.width - 18}x1"
+ a.xc Configuration.instance.header_background
+ a.size.+
+ if Configuration.instance.header_font.exists?
+ a.font Configuration.instance.header_font.path
+ end
+ a.pointsize Configuration.instance.header_font.size
+ a.background Configuration.instance.header_background
+ a.fill Configuration.instance.header_color
+ a.stack do |b|
+ b.gravity 'West'
+ b.stack do |c|
+ c.label 'Filename: '
+ if Configuration.instance.header_font.exists?
+ c.font Configuration.instance.header_font.path
+ end
+ c.label File.basename(@video.full_path)
+ c.append.+
+ end
+ if Configuration.instance.header_font.exists?
+ b.font Configuration.instance.header_font.path
+ end
+ b.label "File size: #{Tools.to_human_size(File.size(@video.full_path))}"
+ b.label "Length: #{@length.to_timestamp}"
+ b.append
+ b.crop "#{montage.width}x#{header_height}+0+0"
+ end
+ a.append
+ a.stack do |b|
+ b.size "#{montage.width}x#{header_height}"
+ b.gravity 'East'
+ b.fill Configuration.instance.header_color
+ b.annotate '+0-1'
+ b << "Dimensions: #{@video.video.width}x#{@video.video.height}\nFormat: #{@video.video.codec(true)} / #{@video.audio ? @video.audio.codec(true) : 'no audio'}\nFPS: #{"%.02f" % @video.video.frame_rate.to_f}"
+ end
+ a.bordercolor Configuration.instance.header_background
+ a.border 9
+ end
+ convert << create_title(montage) if @title
+ convert << create_highlight(montage) if @highlight
+ convert << montage.path
+ convert.append
+ if @signature && @signature.length > 0
+ convert.stack do |a|
+ a.size "#{montage.width}x#{signature_height}"
+ a.gravity 'Center'
+ a.xc Configuration.instance.signature_background
+ if Configuration.instance.signature_font.exists?
+ a.font Configuration.instance.signature_font.path
+ end
+ a.pointsize Configuration.instance.signature_font.size
+ a.fill Configuration.instance.signature_color
+ a.annotate(0, @signature)
+ end
+ convert.append
+ end
+ if @format == :jpg || @format == :jpeg
+ convert.quality(Configuration.instance.quality)
+ end
+ convert << file_path
+ end
+ file_path
+ end
+ end
+end