vcs_ruby 1.1.8 → 1.1.9

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/contact_sheet.rb CHANGED
@@ -1,366 +1,380 @@
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 :signature, :title, :highlight
14
- attr_accessor :softshadow, :timestamp, :polaroid
15
- attr_reader :thumbnail_width, :thumbnail_height, :format
16
- attr_reader :length, :from, :to
17
-
18
- def initialize video, capturer
19
- @video = video
20
- @capturer = capturer
21
- @signature = "Created by Video Contact Sheet Ruby"
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
-
30
- detect_video_properties
31
-
32
- @from = TimeIndex.new 0
33
- @to = @video.info.duration
34
-
35
- @timestamp = Configuration.instance.timestamp
36
- @softshadow = Configuration.instance.softshadow
37
- @polaroid = Configuration.instance.polaroid
38
-
39
- @tempdir = Dir.mktmpdir
40
-
41
- ObjectSpace.define_finalizer(self, self.class.finalize(@tempdir) )
42
- initialize_geometry(Configuration.instance.rows, Configuration.instance.columns, Configuration.instance.interval)
43
-
44
- end
45
-
46
- def initialize_filename override = nil
47
- @out_path = File.dirname(@video.full_path)
48
- @out_filename = File.basename(override || @video.full_path,'.*')
49
-
50
- extension = override ? File.extname(override) : ''
51
- @format = extension.length > 0 ? extension : '.png'
52
- end
53
-
54
- def initialize_geometry(rows, columns, interval)
55
- @has_interval = !!interval
56
- @rows = rows
57
- @columns = columns
58
- @interval = interval
59
- end
60
-
61
- def filename
62
- "#{@out_filename}#{@format}"
63
- end
64
-
65
- def format= format
66
- @format = ".#{format.to_s}"
67
- end
68
-
69
- def full_path
70
- File.join(@out_path, filename)
71
- end
72
-
73
- def rows
74
- @rows
75
- end
76
-
77
- def columns
78
- @columns
79
- end
80
-
81
- def interval
82
- @interval || (@to - @from) / (number_of_caps)
83
- end
84
-
85
- def number_of_caps
86
- if @has_interval
87
- (@to - @from) / @interval
88
- else
89
- if @rows && @columns
90
- @rows * @columns
91
- else
92
- raise "you need at least 2 parameters from columns, rows and interval"
93
- end
94
- end
95
- end
96
-
97
- def thumbnail_width= width
98
- @thumbnail_height = (width.to_f / @thumbnail_width * thumbnail_height).to_i
99
- @thumbnail_width = width
100
- end
101
-
102
- def thumbnail_height= height
103
- @thumbnail_width = (height.to_f / @thumbnail_height * thumbnail_width).to_i
104
- @thumbnail_height = height
105
- end
106
-
107
- def from= time
108
- if (TimeIndex.new(0) < time) && (time < to) && (time < @length)
109
- @from = time
110
- else
111
- raise "Invalid From Time"
112
- end
113
- end
114
-
115
- def to= time
116
- if (TimeIndex.new(0) < time) && (from < time) && (time < @length)
117
- @to = time
118
- else
119
- raise "Invalid To Time"
120
- end
121
- end
122
-
123
- def build
124
- if (@video.info.duration.total_seconds < 1.0)
125
- puts "Video is shorter than 1 sec"
126
- else
127
- initialize_filters
128
- initialize_thumbnails
129
- capture_thumbnails
130
-
131
- puts "Composing standard contact sheet..." unless Configuration.instance.quiet?
132
- montage = splice_montage(montage_thumbs)
133
-
134
- image = MiniMagick::Image.open(montage)
135
-
136
- puts "Adding header and footer..." unless Configuration.instance.quiet?
137
- final = add_header_and_footer image
138
-
139
- puts "Done. Output wrote to '#{filename}'" unless Configuration.instance.quiet?
140
- FileUtils.mv(final, full_path)
141
- end
142
- end
143
-
144
- private
145
- def self.finalize(tempdir)
146
- proc do
147
- puts "Cleaning up..." unless Configuration.instance.quiet?
148
- FileUtils.rm_r tempdir
149
- end
150
- end
151
-
152
- def initialize_filters
153
- @filters = []
154
- @filters << :resize_filter
155
- @filters << :softshadow_filter if softshadow
156
- @filters << :timestamp_filter if timestamp
157
- @filters << :polaroid_filter if polaroid
158
- end
159
-
160
- def initialize_thumbnails
161
- @thumbnails = []
162
- time = @from + (interval / 2)
163
- (1..number_of_caps).each do |i|
164
- thumb = Frame.new @video, @capturer, time
165
- time = time + interval
166
- thumb.width = thumbnail_width
167
- thumb.height = thumbnail_height
168
- thumb.filename = File::join(@tempdir, "th#{"%03d" % i}.#{@capturer.format_extension}")
169
- thumb.filters.push(*@filters)
170
-
171
- @thumbnails << thumb
172
- end
173
- end
174
-
175
- def capture_thumbnails
176
- puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Configuration.instance.quiet?
177
-
178
- @thumbnails.each_with_index do |thumbnail, i|
179
- puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Configuration.instance.quiet?
180
- if Configuration.instance.blank_evasion?
181
- thumbnail.capture_and_evade interval
182
- else
183
- thumbnail.capture
184
- end
185
- thumbnail.apply_filters
186
- end
187
- end
188
-
189
- def detect_video_properties
190
- detect_length
191
- detect_dimensions
192
- end
193
-
194
- def detect_length
195
- @length = @video.info.duration
196
-
197
- @from = TimeIndex.new 0.0
198
- @to = @length
199
- end
200
-
201
- def detect_dimensions
202
- @thumbnail_width = @video.video.width
203
- @thumbnail_height = @video.video.height
204
- end
205
-
206
- def montage_thumbs
207
- file_path = File::join(@tempdir, 'montage.png')
208
- MiniMagick::Tool::Montage.new do |montage|
209
- montage.background Configuration.instance.contact_background
210
- @thumbnails.each do |thumbnail|
211
- montage << thumbnail.filename
212
- end
213
- montage.geometry "+#{Configuration.instance.padding}+#{Configuration.instance.padding}"
214
- # rows or columns can be nil (auto fit)
215
- montage.tile "#{@columns}x#{@rows}"
216
- montage << file_path
217
- end
218
- return file_path
219
- end
220
-
221
- def splice_montage montage_path
222
- if softshadow
223
- left = Configuration.instance.padding + 3
224
- top = Configuration.instance.padding + 5
225
- bottom = right = Configuration.instance.padding
226
- else
227
- left = right = top = bottom = Configuration.instance.padding
228
- end
229
-
230
-
231
- file_path = File::join(@tempdir, 'spliced.png')
232
- MiniMagick::Tool::Convert.new do |convert|
233
- convert << montage_path
234
- convert.background Configuration.instance.contact_background
235
-
236
- convert.splice "#{left}x#{top}"
237
- convert.gravity 'SouthEast'
238
- convert.splice "#{right}x#{bottom}"
239
-
240
- convert << file_path
241
- end
242
- file_path
243
- end
244
-
245
- def create_title montage
246
- file_path = File::join(@tempdir, 'title.png')
247
- MiniMagick::Tool::Convert.new do |convert|
248
- convert.stack do |ul|
249
- ul.size "#{montage.width}x#{Configuration.instance.title_font.line_height}"
250
- ul.xc Configuration.instance.title_background
251
- if Configuration.instance.title_font.exists?
252
- ul.font Configuration.instance.title_font.path
253
- end
254
- ul.pointsize Configuration.instance.title_font.size
255
- ul.background Configuration.instance.title_background
256
- ul.fill Configuration.instance.title_color
257
- ul.gravity 'Center'
258
- ul.annotate(0, @title)
259
- end
260
- convert.flatten
261
- convert << file_path
262
- end
263
- return file_path
264
- end
265
-
266
- def create_highlight montage
267
- puts "Generating highlight..."
268
- thumb = Frame.new @video, @highlight
269
-
270
- thumb.width = thumbnail_width
271
- thumb.height = thumbnail_height
272
- thumb.filename = File::join(@tempdir, "highlight_thumb.png")
273
- thumb.capture
274
- thumb.apply_filters
275
-
276
- file_path = File::join(@tempdir, "highlight.png")
277
- MiniMagick::Tool::Convert.new do |convert|
278
- convert.stack do |a|
279
- a.size "#{montage.width}x#{thumbnail_height+20}"
280
- a.xc Configuration.instance.highlight_background
281
- a.gravity 'Center'
282
- a << thumb.filename
283
- a.composite
284
- end
285
- convert.stack do |a|
286
- a.size "#{montage.width}x1"
287
- a.xc 'Black'
288
- end
289
- convert.append
290
- convert << file_path
291
- end
292
-
293
- file_path
294
- end
295
-
296
- def add_header_and_footer montage
297
- file_path = File::join(@tempdir, filename)
298
- header_height = Configuration.instance.header_font.line_height * 3
299
- signature_height = Configuration.instance.signature_font.line_height + 8
300
- MiniMagick::Tool::Convert.new do |convert|
301
- convert.stack do |a|
302
- a.size "#{montage.width - 18}x1"
303
- a.xc Configuration.instance.header_background
304
- a.size.+
305
- if Configuration.instance.header_font.exists?
306
- a.font Configuration.instance.header_font.path
307
- end
308
- a.pointsize Configuration.instance.header_font.size
309
- a.background Configuration.instance.header_background
310
- a.fill Configuration.instance.header_color
311
- a.stack do |b|
312
- b.gravity 'West'
313
- b.stack do |c|
314
- c.label 'Filename: '
315
- if Configuration.instance.header_font.exists?
316
- c.font Configuration.instance.header_font.path
317
- end
318
- c.label File.basename(@video.full_path)
319
- c.append.+
320
- end
321
- if Configuration.instance.header_font.exists?
322
- b.font Configuration.instance.header_font.path
323
- end
324
- b.label "File size: #{Tools.to_human_size(File.size(@video.full_path))}"
325
- b.label "Length: #{@length.to_timestamp}"
326
- b.append
327
- b.crop "#{montage.width}x#{header_height}+0+0"
328
- end
329
- a.append
330
- a.stack do |b|
331
- b.size "#{montage.width}x#{header_height}"
332
- b.gravity 'East'
333
- b.fill Configuration.instance.header_color
334
- b.annotate '+0-1'
335
- 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}"
336
- end
337
- a.bordercolor Configuration.instance.header_background
338
- a.border 9
339
- end
340
- convert << create_title(montage) if @title
341
- convert << create_highlight(montage) if @highlight
342
- convert << montage.path
343
- convert.append
344
- if @signature && @signature.length > 0
345
- convert.stack do |a|
346
- a.size "#{montage.width}x#{signature_height}"
347
- a.gravity 'Center'
348
- a.xc Configuration.instance.signature_background
349
- if Configuration.instance.signature_font.exists?
350
- a.font Configuration.instance.signature_font.path
351
- end
352
- a.pointsize Configuration.instance.signature_font.size
353
- a.fill Configuration.instance.signature_color
354
- a.annotate(0, @signature)
355
- end
356
- convert.append
357
- end
358
- if @format == :jpg || @format == :jpeg
359
- convert.quality(Configuration.instance.quality)
360
- end
361
- convert << file_path
362
- end
363
- file_path
364
- end
365
- end
366
- end
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 :signature, :title, :highlight
14
+ attr_accessor :softshadow, :timestamp, :polaroid
15
+ attr_reader :thumbnail_width, :thumbnail_height, :aspect_ratio, :format
16
+ attr_reader :length, :from, :to
17
+
18
+ def initialize video, capturer
19
+ @video = video
20
+ @capturer = capturer
21
+ @signature = "Created by Video Contact Sheet Ruby"
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
+
30
+ detect_video_properties
31
+
32
+ @from = TimeIndex.new 0
33
+ @to = @video.info.duration
34
+
35
+ @timestamp = Configuration.instance.timestamp
36
+ @softshadow = Configuration.instance.softshadow
37
+ @polaroid = Configuration.instance.polaroid
38
+
39
+ @tempdir = Dir.mktmpdir
40
+
41
+ ObjectSpace.define_finalizer(self, self.class.finalize(@tempdir) )
42
+ initialize_geometry(Configuration.instance.rows, Configuration.instance.columns, Configuration.instance.interval)
43
+
44
+ end
45
+
46
+ def initialize_filename override = nil
47
+ @out_path = File.dirname(@video.full_path)
48
+ @out_filename = File.basename(override || @video.full_path,'.*')
49
+
50
+ extension = override ? File.extname(override) : ''
51
+ @format = extension.length > 0 ? extension : '.png'
52
+ end
53
+
54
+ def initialize_geometry(rows, columns, interval)
55
+ @has_interval = !!interval
56
+ @rows = rows
57
+ @columns = columns
58
+ @interval = interval
59
+ end
60
+
61
+ def filename
62
+ "#{@out_filename}#{@format}"
63
+ end
64
+
65
+ def format= format
66
+ @format = ".#{format.to_s}"
67
+ end
68
+
69
+ def full_path
70
+ File.join(@out_path, filename)
71
+ end
72
+
73
+ def rows
74
+ @rows
75
+ end
76
+
77
+ def columns
78
+ @columns
79
+ end
80
+
81
+ def interval
82
+ @interval || (@to - @from) / (number_of_caps)
83
+ end
84
+
85
+ def number_of_caps
86
+ if @has_interval
87
+ (@to - @from) / @interval
88
+ else
89
+ if @rows && @columns
90
+ @rows * @columns
91
+ else
92
+ raise "you need at least 2 parameters from columns, rows and interval"
93
+ end
94
+ end
95
+ end
96
+
97
+ def thumbnail_width= width
98
+ @thumbnail_height = (width.to_f / @thumbnail_width * thumbnail_height).to_i
99
+ @thumbnail_width = width
100
+ end
101
+
102
+ def thumbnail_height= height
103
+ @thumbnail_width = (height.to_f / @thumbnail_height * thumbnail_width).to_i
104
+ @thumbnail_height = height
105
+ end
106
+
107
+ def aspect_ratio= aspect_ratio
108
+ @thumbnail_width = (@thumbnail_height * aspect_ratio).to_i
109
+ @aspect_ratio = aspect_ratio
110
+ end
111
+
112
+ def from= time
113
+ if (TimeIndex.new(0) < time) && (time < to) && (time < @length)
114
+ @from = time
115
+ else
116
+ raise "Invalid From Time"
117
+ end
118
+ end
119
+
120
+ def to= time
121
+ if (TimeIndex.new(0) < time) && (from < time) && (time < @length)
122
+ @to = time
123
+ else
124
+ raise "Invalid To Time"
125
+ end
126
+ end
127
+
128
+ def build
129
+ if (@video.info.duration.total_seconds < 1.0)
130
+ puts "Video is shorter than 1 sec"
131
+ else
132
+ initialize_filters
133
+ initialize_thumbnails
134
+ capture_thumbnails
135
+
136
+ puts "Composing standard contact sheet..." unless Configuration.instance.quiet?
137
+ montage = splice_montage(montage_thumbs)
138
+
139
+ image = MiniMagick::Image.open(montage)
140
+
141
+ puts "Adding header and footer..." unless Configuration.instance.quiet?
142
+ final = add_header_and_footer image
143
+
144
+ puts "Done. Output wrote to '#{filename}'" unless Configuration.instance.quiet?
145
+ FileUtils.mv(final, full_path)
146
+ end
147
+ end
148
+
149
+ private
150
+ def self.finalize(tempdir)
151
+ proc do
152
+ puts "Cleaning up..." unless Configuration.instance.quiet?
153
+ FileUtils.rm_r tempdir
154
+ end
155
+ end
156
+
157
+ def initialize_filters
158
+ @filters = []
159
+ @filters << :resize_filter
160
+ @filters << :softshadow_filter if softshadow
161
+ @filters << :timestamp_filter if timestamp
162
+ @filters << :polaroid_filter if polaroid
163
+ end
164
+
165
+ def initialize_thumbnails
166
+ @thumbnails = []
167
+ time = @from + (interval / 2)
168
+ (1..number_of_caps).each do |i|
169
+ thumb = Frame.new @video, @capturer, time
170
+ time = time + interval
171
+ thumb.width = thumbnail_width
172
+ thumb.height = thumbnail_height
173
+ thumb.filename = File::join(@tempdir, "th#{"%03d" % i}.#{@capturer.format_extension}")
174
+ thumb.filters.push(*@filters)
175
+
176
+ @thumbnails << thumb
177
+ end
178
+ end
179
+
180
+ def capture_thumbnails
181
+ puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Configuration.instance.quiet?
182
+
183
+ @thumbnails.each_with_index do |thumbnail, i|
184
+ puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Configuration.instance.quiet?
185
+ if Configuration.instance.blank_evasion?
186
+ thumbnail.capture_and_evade interval
187
+ else
188
+ thumbnail.capture
189
+ end
190
+ thumbnail.apply_filters
191
+ end
192
+ end
193
+
194
+ def detect_video_properties
195
+ detect_length
196
+ detect_dimensions
197
+ end
198
+
199
+ def detect_length
200
+ @length = @video.info.duration
201
+
202
+ @from = TimeIndex.new 0.0
203
+ @to = @length
204
+ end
205
+
206
+ def detect_dimensions
207
+ @thumbnail_width = @video.video.width
208
+ @thumbnail_height = @video.video.height
209
+
210
+ @aspect_ratio = @video.video.aspect_ratio
211
+
212
+ if @aspect_ratio == 0
213
+ @aspect_ratio = Rational(@video.video.width, @video.video.height)
214
+ else
215
+ #recalculate width, for PAR 1:1 this should be the same as before
216
+ @thumbnail_width = (@aspect_ratio * @video.video.height).to_i
217
+ end
218
+ end
219
+
220
+ def montage_thumbs
221
+ file_path = File::join(@tempdir, 'montage.png')
222
+ MiniMagick::Tool::Montage.new do |montage|
223
+ montage.background Configuration.instance.contact_background
224
+ @thumbnails.each do |thumbnail|
225
+ montage << thumbnail.filename
226
+ end
227
+ montage.geometry "+#{Configuration.instance.padding}+#{Configuration.instance.padding}"
228
+ # rows or columns can be nil (auto fit)
229
+ montage.tile "#{@columns}x#{@rows}"
230
+ montage << file_path
231
+ end
232
+ return file_path
233
+ end
234
+
235
+ def splice_montage montage_path
236
+ if softshadow
237
+ left = Configuration.instance.padding + 3
238
+ top = Configuration.instance.padding + 5
239
+ bottom = right = Configuration.instance.padding
240
+ else
241
+ left = right = top = bottom = Configuration.instance.padding
242
+ end
243
+
244
+
245
+ file_path = File::join(@tempdir, 'spliced.png')
246
+ MiniMagick::Tool::Convert.new do |convert|
247
+ convert << montage_path
248
+ convert.background Configuration.instance.contact_background
249
+
250
+ convert.splice "#{left}x#{top}"
251
+ convert.gravity 'SouthEast'
252
+ convert.splice "#{right}x#{bottom}"
253
+
254
+ convert << file_path
255
+ end
256
+ file_path
257
+ end
258
+
259
+ def create_title montage
260
+ file_path = File::join(@tempdir, 'title.png')
261
+ MiniMagick::Tool::Convert.new do |convert|
262
+ convert.stack do |ul|
263
+ ul.size "#{montage.width}x#{Configuration.instance.title_font.line_height}"
264
+ ul.xc Configuration.instance.title_background
265
+ if Configuration.instance.title_font.exists?
266
+ ul.font Configuration.instance.title_font.path
267
+ end
268
+ ul.pointsize Configuration.instance.title_font.size
269
+ ul.background Configuration.instance.title_background
270
+ ul.fill Configuration.instance.title_color
271
+ ul.gravity 'Center'
272
+ ul.annotate(0, @title)
273
+ end
274
+ convert.flatten
275
+ convert << file_path
276
+ end
277
+ return file_path
278
+ end
279
+
280
+ def create_highlight montage
281
+ puts "Generating highlight..."
282
+ thumb = Frame.new @video, @highlight
283
+
284
+ thumb.width = thumbnail_width
285
+ thumb.height = thumbnail_height
286
+ thumb.filename = File::join(@tempdir, "highlight_thumb.png")
287
+ thumb.capture
288
+ thumb.apply_filters
289
+
290
+ file_path = File::join(@tempdir, "highlight.png")
291
+ MiniMagick::Tool::Convert.new do |convert|
292
+ convert.stack do |a|
293
+ a.size "#{montage.width}x#{thumbnail_height+20}"
294
+ a.xc Configuration.instance.highlight_background
295
+ a.gravity 'Center'
296
+ a << thumb.filename
297
+ a.composite
298
+ end
299
+ convert.stack do |a|
300
+ a.size "#{montage.width}x1"
301
+ a.xc 'Black'
302
+ end
303
+ convert.append
304
+ convert << file_path
305
+ end
306
+
307
+ file_path
308
+ end
309
+
310
+ def add_header_and_footer montage
311
+ file_path = File::join(@tempdir, filename)
312
+ header_height = Configuration.instance.header_font.line_height * 3
313
+ signature_height = Configuration.instance.signature_font.line_height + 8
314
+ MiniMagick::Tool::Convert.new do |convert|
315
+ convert.stack do |a|
316
+ a.size "#{montage.width - 18}x1"
317
+ a.xc Configuration.instance.header_background
318
+ a.size.+
319
+ if Configuration.instance.header_font.exists?
320
+ a.font Configuration.instance.header_font.path
321
+ end
322
+ a.pointsize Configuration.instance.header_font.size
323
+ a.background Configuration.instance.header_background
324
+ a.fill Configuration.instance.header_color
325
+ a.stack do |b|
326
+ b.gravity 'West'
327
+ b.stack do |c|
328
+ c.label 'Filename: '
329
+ if Configuration.instance.header_font.exists?
330
+ c.font Configuration.instance.header_font.path
331
+ end
332
+ c.label File.basename(@video.full_path)
333
+ c.append.+
334
+ end
335
+ if Configuration.instance.header_font.exists?
336
+ b.font Configuration.instance.header_font.path
337
+ end
338
+ b.label "File size: #{Tools.to_human_size(File.size(@video.full_path))}"
339
+ b.label "Length: #{@length.to_timestamp}"
340
+ b.append
341
+ b.crop "#{montage.width}x#{header_height}+0+0"
342
+ end
343
+ a.append
344
+ a.stack do |b|
345
+ b.size "#{montage.width}x#{header_height}"
346
+ b.gravity 'East'
347
+ b.fill Configuration.instance.header_color
348
+ b.annotate '+0-1'
349
+ 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}"
350
+ end
351
+ a.bordercolor Configuration.instance.header_background
352
+ a.border 9
353
+ end
354
+ convert << create_title(montage) if @title
355
+ convert << create_highlight(montage) if @highlight
356
+ convert << montage.path
357
+ convert.append
358
+ if @signature && @signature.length > 0
359
+ convert.stack do |a|
360
+ a.size "#{montage.width}x#{signature_height}"
361
+ a.gravity 'Center'
362
+ a.xc Configuration.instance.signature_background
363
+ if Configuration.instance.signature_font.exists?
364
+ a.font Configuration.instance.signature_font.path
365
+ end
366
+ a.pointsize Configuration.instance.signature_font.size
367
+ a.fill Configuration.instance.signature_color
368
+ a.annotate(0, @signature)
369
+ end
370
+ convert.append
371
+ end
372
+ if @format == :jpg || @format == :jpeg
373
+ convert.quality(Configuration.instance.quality)
374
+ end
375
+ convert << file_path
376
+ end
377
+ file_path
378
+ end
379
+ end
380
+ end