bulldog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/.gitignore +2 -0
  2. data/DESCRIPTION.txt +3 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +64 -0
  6. data/VERSION +1 -0
  7. data/bulldog.gemspec +157 -0
  8. data/lib/bulldog.rb +95 -0
  9. data/lib/bulldog/attachment.rb +49 -0
  10. data/lib/bulldog/attachment/base.rb +167 -0
  11. data/lib/bulldog/attachment/has_dimensions.rb +94 -0
  12. data/lib/bulldog/attachment/image.rb +63 -0
  13. data/lib/bulldog/attachment/maybe.rb +229 -0
  14. data/lib/bulldog/attachment/none.rb +37 -0
  15. data/lib/bulldog/attachment/pdf.rb +63 -0
  16. data/lib/bulldog/attachment/unknown.rb +11 -0
  17. data/lib/bulldog/attachment/video.rb +143 -0
  18. data/lib/bulldog/error.rb +5 -0
  19. data/lib/bulldog/has_attachment.rb +214 -0
  20. data/lib/bulldog/interpolation.rb +73 -0
  21. data/lib/bulldog/missing_file.rb +12 -0
  22. data/lib/bulldog/processor.rb +5 -0
  23. data/lib/bulldog/processor/argument_tree.rb +116 -0
  24. data/lib/bulldog/processor/base.rb +124 -0
  25. data/lib/bulldog/processor/ffmpeg.rb +172 -0
  26. data/lib/bulldog/processor/image_magick.rb +134 -0
  27. data/lib/bulldog/processor/one_shot.rb +19 -0
  28. data/lib/bulldog/reflection.rb +234 -0
  29. data/lib/bulldog/saved_file.rb +19 -0
  30. data/lib/bulldog/stream.rb +186 -0
  31. data/lib/bulldog/style.rb +38 -0
  32. data/lib/bulldog/style_set.rb +101 -0
  33. data/lib/bulldog/tempfile.rb +28 -0
  34. data/lib/bulldog/util.rb +92 -0
  35. data/lib/bulldog/validations.rb +68 -0
  36. data/lib/bulldog/vector2.rb +18 -0
  37. data/rails/init.rb +9 -0
  38. data/script/console +8 -0
  39. data/spec/data/empty.txt +0 -0
  40. data/spec/data/test.jpg +0 -0
  41. data/spec/data/test.mov +0 -0
  42. data/spec/data/test.pdf +0 -0
  43. data/spec/data/test.png +0 -0
  44. data/spec/data/test2.jpg +0 -0
  45. data/spec/helpers/image_creation.rb +8 -0
  46. data/spec/helpers/temporary_directory.rb +25 -0
  47. data/spec/helpers/temporary_models.rb +76 -0
  48. data/spec/helpers/temporary_values.rb +102 -0
  49. data/spec/helpers/test_upload_files.rb +108 -0
  50. data/spec/helpers/time_travel.rb +20 -0
  51. data/spec/integration/data/test.jpg +0 -0
  52. data/spec/integration/lifecycle_hooks_spec.rb +213 -0
  53. data/spec/integration/processing_image_attachments.rb +72 -0
  54. data/spec/integration/processing_video_attachments_spec.rb +82 -0
  55. data/spec/integration/saving_an_attachment_spec.rb +31 -0
  56. data/spec/matchers/file_operations.rb +159 -0
  57. data/spec/spec_helper.rb +76 -0
  58. data/spec/unit/attachment/base_spec.rb +311 -0
  59. data/spec/unit/attachment/image_spec.rb +128 -0
  60. data/spec/unit/attachment/maybe_spec.rb +126 -0
  61. data/spec/unit/attachment/pdf_spec.rb +137 -0
  62. data/spec/unit/attachment/video_spec.rb +176 -0
  63. data/spec/unit/attachment_spec.rb +61 -0
  64. data/spec/unit/has_attachment_spec.rb +700 -0
  65. data/spec/unit/interpolation_spec.rb +108 -0
  66. data/spec/unit/processor/argument_tree_spec.rb +159 -0
  67. data/spec/unit/processor/ffmpeg_spec.rb +467 -0
  68. data/spec/unit/processor/image_magick_spec.rb +260 -0
  69. data/spec/unit/processor/one_shot_spec.rb +70 -0
  70. data/spec/unit/reflection_spec.rb +338 -0
  71. data/spec/unit/stream_spec.rb +234 -0
  72. data/spec/unit/style_set_spec.rb +44 -0
  73. data/spec/unit/style_spec.rb +51 -0
  74. data/spec/unit/validations_spec.rb +491 -0
  75. data/spec/unit/vector2_spec.rb +27 -0
  76. data/tasks/bulldog_tasks.rake +4 -0
  77. metadata +193 -0
@@ -0,0 +1,12 @@
1
+ module Bulldog
2
+ class MissingFile
3
+ def initialize(options={})
4
+ @attachment_type = attachment_type
5
+ @file_name = options[:file_name] || 'missing-file'
6
+ @content_type = options[:content_type]
7
+ @path = options[:path] || '/dev/null'
8
+ end
9
+
10
+ attr_reader :attachment_type, :file_name, :content_type, :path
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ require 'bulldog/processor/argument_tree'
2
+ require 'bulldog/processor/base'
3
+ require 'bulldog/processor/one_shot'
4
+ require 'bulldog/processor/image_magick'
5
+ require 'bulldog/processor/ffmpeg'
@@ -0,0 +1,116 @@
1
+ module Bulldog
2
+ module Processor
3
+ class ArgumentTree
4
+ def initialize(styles)
5
+ @styles = styles
6
+ @root = Node.new(styles)
7
+ @heads = {}
8
+ styles.each{|s| @heads[s] = @root}
9
+ end
10
+
11
+ attr_reader :styles, :root, :heads
12
+
13
+ def add(style, arguments, &callback)
14
+ # Assume that if the arguments are the same for a node, the
15
+ # callback will be identical.
16
+ child = heads[style].children.find do |node|
17
+ node.arguments == arguments
18
+ end
19
+ if child
20
+ child.styles << style
21
+ else
22
+ child = Node.new([style], arguments, &callback)
23
+ heads[style].children << child
24
+ end
25
+ heads[style] = child
26
+ end
27
+
28
+ def output(style, path)
29
+ heads[style].outputs << path
30
+ end
31
+
32
+ def inspect
33
+ io = StringIO.new
34
+ inspect_node(io, @root)
35
+ io.string
36
+ end
37
+
38
+ #
39
+ # Return the list of arguments the tree represents.
40
+ #
41
+ def arguments
42
+ arguments = visit_node_for_arguments([], root, false)
43
+ # Don't specify -write for the last output file.
44
+ arguments[-2] == '-write' or
45
+ raise "[BULLDOG BUG]: expected second last argument to be -write in: #{arguments.inspect}"
46
+ arguments.delete_at(-2)
47
+ arguments
48
+ end
49
+
50
+ #
51
+ # Yield each callback, along with the styles they apply to, in
52
+ # the order they appear in the tree.
53
+ #
54
+ def each_callback(&block)
55
+ visit_node_for_callbacks(root, block)
56
+ end
57
+
58
+ private # ---------------------------------------------------
59
+
60
+ def inspect_node(io, node, margin='')
61
+ puts "#{margin}* #{node.styles.map(&:name).join(', ')}: #{node.arguments.join(' ')}"
62
+ node.children.each do |child|
63
+ inspect_node(io, child, margin + ' ')
64
+ end
65
+ end
66
+
67
+ def visit_node_for_arguments(arguments, node, clone)
68
+ if clone
69
+ arguments << '(' << '+clone'
70
+ visit_node_for_arguments(arguments, node, false)
71
+ arguments << '+delete' << ')'
72
+ else
73
+ arguments.concat(node.arguments)
74
+ node.outputs.each{|path| arguments << '-write' << path}
75
+
76
+ num_children = node.children.size
77
+ node.children.each_with_index do |child, i|
78
+ # No need to clone the image for the last child.
79
+ visit_node_for_arguments(arguments, child, i < num_children - 1)
80
+ end
81
+ end
82
+ arguments
83
+ end
84
+
85
+ def visit_node_for_callbacks(node, block)
86
+ if node.callback
87
+ block.call(node.styles, node.callback)
88
+ end
89
+ node.children.each do |child|
90
+ visit_node_for_callbacks(child, block)
91
+ end
92
+ end
93
+
94
+ class Node
95
+ def initialize(styles, arguments=[], &callback)
96
+ @styles = styles
97
+ @arguments = arguments
98
+ @callback = callback
99
+ @outputs = []
100
+ @children = []
101
+ end
102
+
103
+ def add_child(child)
104
+ children << child
105
+ end
106
+
107
+ def remove_child(child)
108
+ children.delete(child)
109
+ end
110
+
111
+ attr_accessor :outputs
112
+ attr_reader :styles, :arguments, :callback, :children
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,124 @@
1
+ module Bulldog
2
+ module Processor
3
+ class Base
4
+ def initialize(attachment, styles, input_file)
5
+ @attachment = attachment
6
+ @styles = styles
7
+ @input_file = input_file
8
+ end
9
+
10
+ #
11
+ # The attachment object being processed.
12
+ #
13
+ attr_reader :attachment
14
+
15
+ #
16
+ # The styles to run this processor for.
17
+ #
18
+ attr_reader :styles
19
+
20
+ #
21
+ # The record being processed.
22
+ #
23
+ def record
24
+ attachment.record
25
+ end
26
+
27
+ #
28
+ # The name of the attachment being processed.
29
+ #
30
+ def name
31
+ attachment.name
32
+ end
33
+
34
+ #
35
+ # The name of the original file.
36
+ #
37
+ attr_reader :input_file
38
+
39
+ #
40
+ # The name of the output file for the given style.
41
+ #
42
+ def output_file(style_name)
43
+ overrides = {}
44
+ if (format = styles[style_name][:format])
45
+ overrides[:extension] = format
46
+ end
47
+ attachment.interpolate_path(style_name, overrides)
48
+ end
49
+
50
+ #
51
+ # Return the value of the attachment.
52
+ #
53
+ def value
54
+ record.send(name).value
55
+ end
56
+
57
+ #
58
+ # Run the given block in the context of this processor, once for
59
+ # each style.
60
+ #
61
+ # #style will be set to the current style each time the block is
62
+ # called.
63
+ #
64
+ def process(*args, &block)
65
+ return if styles.empty?
66
+ styles.each do |style|
67
+ @style = style
68
+ begin
69
+ process_style(*args, &block)
70
+ ensure
71
+ @style = nil
72
+ end
73
+ end
74
+ end
75
+
76
+ #
77
+ # The current style being processed.
78
+ #
79
+ attr_reader :style
80
+
81
+ protected # ---------------------------------------------------
82
+
83
+ #
84
+ # Run the given block with #style set to one of the styles to
85
+ # process.
86
+ #
87
+ # This is called by #process for each output style.
88
+ #
89
+ def process_style(*args, &block)
90
+ # Avoid #instance_exec if possible for ruby 1.8.
91
+ evaluator = args.empty? ? :instance_eval : :instance_exec
92
+ send(evaluator, *args, &block) if block
93
+ end
94
+
95
+ def log(level, message)
96
+ logger = Bulldog.logger
97
+ logger.send(level, message) unless logger.nil?
98
+ end
99
+
100
+ def shell_escape(str)
101
+ if RUBY_VERSION >= '1.9'
102
+ Shellwords.shellescape(str)
103
+ else
104
+ # Taken from ruby 1.9.
105
+
106
+ # An empty argument will be skipped, so return empty quotes.
107
+ return "''" if str.empty?
108
+
109
+ str = str.dup
110
+
111
+ # Process as a single byte sequence because not all shell
112
+ # implementations are multibyte aware.
113
+ str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
114
+
115
+ # A LF cannot be escaped with a backslash because a backslash + LF
116
+ # combo is regarded as line continuation and simply ignored.
117
+ str.gsub!(/\n/, "'\n'")
118
+
119
+ return str
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,172 @@
1
+ module Bulldog
2
+ module Processor
3
+ class Ffmpeg < Base
4
+ class << self
5
+ attr_accessor :ffmpeg_command
6
+ end
7
+
8
+ self.ffmpeg_command = Bulldog.find_in_path('ffmpeg')
9
+
10
+ def initialize(*args)
11
+ super
12
+ @operation = nil
13
+ @arguments = style_list_map
14
+ @still_frame_callbacks = style_list_map
15
+ end
16
+
17
+ def process(*args)
18
+ return if styles.empty?
19
+ super
20
+ run_ffmpeg
21
+ run_still_frame_callbacks
22
+ end
23
+
24
+ def process_style(*args)
25
+ super
26
+ run_default_operation
27
+ end
28
+
29
+ def use_threads(num_threads)
30
+ operate '-threads', num_threads
31
+ end
32
+
33
+ def encode(params={})
34
+ @operation = :encode
35
+ params = style.attributes.merge(params)
36
+ parse_video_option(params)
37
+ parse_audio_option(params)
38
+ style_option '-vcodec', params[:video_codec]
39
+ style_option '-acodec', params[:audio_codec]
40
+ preset_option '-vpre', params[:video_preset]
41
+ preset_option '-apre', params[:audio_preset]
42
+ operate '-s', attachment.dimensions(style.name).join('x') if params[:size]
43
+ style_option '-r', params[:frame_rate]
44
+ style_option '-b', params[:video_bit_rate]
45
+ style_option '-ar', params[:sampling_rate]
46
+ style_option '-ab', params[:audio_bit_rate]
47
+ style_option '-ac', params[:channels]
48
+ operate '-deinterlace' if params[:deinterlaced]
49
+ style_option '-pix_fmt', params[:pixel_format]
50
+ style_option '-b_strategy', params[:b_strategy]
51
+ style_option '-bufsize', params[:buffer_size]
52
+ style_option '-coder', params[:coder]
53
+ style_option '-v', params[:verbosity]
54
+ style_option '-flags', params[:flags]
55
+ preset_option '-spre', params[:subtitle_preset]
56
+ style_option '-y', output_file(style.name)
57
+ end
58
+
59
+ def record_frame(params={}, &block)
60
+ @operation = :record_frame
61
+ params = style.attributes.merge(params)
62
+ operate '-vframes', 1
63
+ operate '-ss', params[:position] || attachment.duration.to_i / 2
64
+ operate '-f', 'image2'
65
+ operate '-vcodec', params[:codec] || default_frame_codec(params)
66
+
67
+ if (attribute = params[:assign_to])
68
+ basename = "recorded_frame.#{params[:format]}"
69
+ output_path = record.send(attribute).interpolate_path(:original, :basename => basename)
70
+ @still_frame_callbacks[style] << lambda do
71
+ file = SavedFile.new(output_path, :file_name => basename)
72
+ record.update_attribute(attribute, file)
73
+ end
74
+ else
75
+ output_path = output_file(style.name)
76
+ end
77
+
78
+ operate '-y', output_path
79
+ if block
80
+ @still_frame_callbacks[style] << lambda{instance_exec(output_path, &block)}
81
+ end
82
+ end
83
+
84
+ private # -----------------------------------------------------
85
+
86
+ def style_list_map
87
+ hash = {}
88
+ styles.each{|s| hash[s] = []}
89
+ hash
90
+ end
91
+
92
+ def operate(*args)
93
+ @arguments[style].concat args.map(&:to_s)
94
+ end
95
+
96
+ def run_default_operation
97
+ encode if @operation.nil?
98
+ end
99
+
100
+ def parse_video_option(params)
101
+ value = params.delete(:video) or
102
+ return
103
+ value.split.each do |word|
104
+ case word
105
+ when /fps\z/i
106
+ params[:frame_rate] = $`
107
+ when /bps\z/i
108
+ params[:video_bit_rate] = $`
109
+ else
110
+ params[:video_codec] = word
111
+ end
112
+ end
113
+ end
114
+
115
+ def parse_audio_option(params)
116
+ value = params.delete(:audio) or
117
+ return
118
+ value.split.each do |word|
119
+ case word
120
+ when /hz\z/i
121
+ params[:sampling_rate] = $`
122
+ when /bps\z/i
123
+ params[:audio_bit_rate] = $`
124
+ when 'mono'
125
+ params[:channels] = 1
126
+ when 'stereo'
127
+ params[:channels] = 2
128
+ else
129
+ params[:audio_codec] = word
130
+ end
131
+ end
132
+ end
133
+
134
+ def style_option(option, *args)
135
+ operate(option, *args) if args.all?
136
+ end
137
+
138
+ def preset_option(option, value)
139
+ Array(value).each do |preset|
140
+ operate option, preset
141
+ end
142
+ end
143
+
144
+ def default_frame_codec(params)
145
+ case params[:format].to_s
146
+ when /jpe?g/i
147
+ 'mjpeg'
148
+ when /png/i
149
+ 'png'
150
+ else
151
+ format = params[:format]
152
+ raise ProcessingError, "no default codec for '#{format}' - please use :codec to specify"
153
+ end
154
+ end
155
+
156
+ def run_ffmpeg
157
+ @arguments.each do |style, arguments|
158
+ command = [self.class.ffmpeg_command]
159
+ command << '-i' << input_file
160
+ command.concat(arguments)
161
+ Bulldog.run(*command)
162
+ end
163
+ end
164
+
165
+ def run_still_frame_callbacks
166
+ @still_frame_callbacks.each do |style, callbacks|
167
+ callbacks.each{|c| c.call}
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,134 @@
1
+ require 'stringio'
2
+
3
+ module Bulldog
4
+ module Processor
5
+ class ImageMagick < Base
6
+ class << self
7
+ attr_accessor :convert_command
8
+ attr_accessor :identify_command
9
+ end
10
+
11
+ self.convert_command = Bulldog.find_in_path('convert')
12
+ self.identify_command = Bulldog.find_in_path('identify')
13
+
14
+ def initialize(*args)
15
+ super
16
+ @tree = ArgumentTree.new(styles)
17
+ end
18
+
19
+ def process(*args, &block)
20
+ return if styles.empty?
21
+ super
22
+ run_convert
23
+ end
24
+
25
+ # Input image attributes --------------------------------------
26
+
27
+ #
28
+ # Yield the dimensions of the generated file at this point in
29
+ # the pipeline.
30
+ #
31
+ # The block is called when `convert' is run after the processor
32
+ # block is evaluated for all styles. If you just need the
33
+ # dimensions of the input file, see Attribute::Photo#dimensions.
34
+ #
35
+ def dimensions(&block)
36
+ operate '-format', '%w %h'
37
+ operate '-identify' do |styles, output|
38
+ width, height = output.gets.split.map(&:to_i)
39
+ block.call(styles, width, height)
40
+ end
41
+ end
42
+
43
+ private # -----------------------------------------------------
44
+
45
+ # Image operations --------------------------------------------
46
+
47
+ def resize
48
+ operate '-resize', style[:size]
49
+ end
50
+
51
+ def auto_orient
52
+ operate '-auto-orient'
53
+ end
54
+
55
+ def strip
56
+ operate '-strip'
57
+ end
58
+
59
+ def flip
60
+ operate '-flip'
61
+ end
62
+
63
+ def flop
64
+ operate '-flop'
65
+ end
66
+
67
+ def rotate(angle)
68
+ unless angle.to_i.zero?
69
+ operate '-rotate', angle.to_s
70
+ end
71
+ end
72
+
73
+ def crop(params)
74
+ operate '-crop', geometry(params[:size], params[:origin])
75
+ operate '+repage'
76
+ end
77
+
78
+ def thumbnail
79
+ if style[:filled]
80
+ operate '-resize', "#{style[:size]}^"
81
+ operate '-gravity', 'Center'
82
+ operate '-crop', "#{style[:size]}+0+0"
83
+ operate '+repage'
84
+ else
85
+ operate '-resize', "#{style[:size]}"
86
+ end
87
+ end
88
+
89
+ private # -----------------------------------------------------
90
+
91
+ def geometry(size, origin=nil)
92
+ size = Vector2.new(size)
93
+ geometry = '%dx%d' % [size.x, size.y]
94
+ if origin
95
+ origin = Vector2.new(origin)
96
+ geometry << ('%+d%+d' % [origin.x, origin.y])
97
+ end
98
+ geometry
99
+ end
100
+
101
+ def operate(*arguments, &block)
102
+ @tree.add(style, arguments, &block)
103
+ end
104
+
105
+ def run_convert
106
+ add_final_style_arguments
107
+ output = run_convert_command and
108
+ run_convert_callbacks(output)
109
+ end
110
+
111
+ def add_final_style_arguments
112
+ styles.each do |style|
113
+ @tree.add(style, ['-quality', style[:quality].to_s]) if style[:quality]
114
+ @tree.add(style, ['-colorspace', style[:colorspace]]) if style[:colorspace]
115
+ path = output_file(style.name)
116
+ FileUtils.mkdir_p(File.dirname(path))
117
+ @tree.output(style, path)
118
+ end
119
+ end
120
+
121
+ def run_convert_command
122
+ command = [self.class.convert_command, "#{input_file}[0]", *@tree.arguments].flatten
123
+ Bulldog.run(*command)
124
+ end
125
+
126
+ def run_convert_callbacks(output)
127
+ io = StringIO.new(output)
128
+ @tree.each_callback do |styles, callback|
129
+ callback.call(styles, io)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end