bulldog 0.0.1

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.
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