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,94 @@
1
+ module Bulldog
2
+ module Attachment
3
+ #
4
+ # Module for dealing with dimensions.
5
+ #
6
+ # Note that due to the way stored attributes are implemented, this
7
+ # module must be included after the definition of #dimensions.
8
+ #
9
+ module HasDimensions
10
+ def self.included(base)
11
+ super
12
+ base.class_eval do
13
+ storable_attribute :width , :per_style => true, :memoize => true
14
+ storable_attribute :height , :per_style => true, :memoize => true
15
+ storable_attribute :aspect_ratio, :per_style => true, :memoize => true
16
+ storable_attribute :dimensions , :per_style => true, :memoize => true, :cast => true
17
+ end
18
+ end
19
+
20
+ #
21
+ # Return the width of the named style.
22
+ #
23
+ # +style_name+ defaults to the attribute's #default_style.
24
+ #
25
+ def width(style_name)
26
+ dimensions(style_name)[0]
27
+ end
28
+
29
+ #
30
+ # Return the height of the named style.
31
+ #
32
+ # +style_name+ defaults to the attribute's #default_style.
33
+ #
34
+ def height(style_name)
35
+ dimensions(style_name)[1]
36
+ end
37
+
38
+ #
39
+ # Return the aspect ratio of the named style.
40
+ #
41
+ # +style_name+ defaults to the attribute's #default_style.
42
+ #
43
+ def aspect_ratio(style_name)
44
+ width(style_name).to_f / height(style_name)
45
+ end
46
+
47
+ #
48
+ # Return the width and height of the named style, as a 2-element
49
+ # array.
50
+ #
51
+ def dimensions
52
+ raise 'abstract method called'
53
+ end
54
+
55
+ protected # -----------------------------------------------------
56
+
57
+ #
58
+ # Return the dimensions, as an array [width, height], that
59
+ # result from resizing +original_dimensions+ to
60
+ # +target_dimensions+. If fill is true, assume the final image
61
+ # will fill the target box. Otherwise the aspect ratio will be
62
+ # maintained.
63
+ #
64
+ def resized_dimensions(original_dimensions, target_dimensions, fill)
65
+ if fill
66
+ target_dimensions
67
+ else
68
+ original_aspect_ratio = original_dimensions[0].to_f / original_dimensions[1]
69
+ target_aspect_ratio = target_dimensions[0].to_f / target_dimensions[1]
70
+ if original_aspect_ratio > target_aspect_ratio
71
+ width = target_dimensions[0]
72
+ height = (width / original_aspect_ratio).round
73
+ else
74
+ height = target_dimensions[1]
75
+ width = (height * original_aspect_ratio).round
76
+ end
77
+ [width, height]
78
+ end
79
+ end
80
+
81
+ private # -----------------------------------------------------
82
+
83
+ def serialize_dimensions(dimensions)
84
+ return nil if dimensions.blank?
85
+ dimensions.join('x')
86
+ end
87
+
88
+ def deserialize_dimensions(string)
89
+ return nil if string.blank?
90
+ string.scan(/\d+/).map(&:to_i)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,63 @@
1
+ module Bulldog
2
+ module Attachment
3
+ class Image < Base
4
+ handle :image
5
+
6
+ #
7
+ # Return the width and height of the named style, as a 2-element
8
+ # array.
9
+ #
10
+ # For :original, this is based on the output of ImageMagick's
11
+ # <tt>identify</tt> command. Other styles are calculated from
12
+ # the original style's dimensions, plus the style's :size and
13
+ # :filled attributes.
14
+ #
15
+ # +style_name+ defaults to the attribute's #default_style.
16
+ #
17
+ def dimensions(style_name)
18
+ if style_name.equal?(:original)
19
+ examine
20
+ @original_dimensions
21
+ else
22
+ style = reflection.styles[style_name]
23
+ target_dimensions = style[:size].split(/x/).map(&:to_i)
24
+ resized_dimensions(dimensions(:original), target_dimensions, style[:filled])
25
+ end
26
+ end
27
+
28
+ include HasDimensions
29
+
30
+ protected # ---------------------------------------------------
31
+
32
+ #
33
+ # Return the default processor class to use for this attachment.
34
+ #
35
+ def default_processor_type
36
+ :image_magick
37
+ end
38
+
39
+ #
40
+ # Read the original image metadata with ImageMagick's identify
41
+ # command.
42
+ #
43
+ def run_examination
44
+ if stream.missing?
45
+ @original_dimensions = [1, 1]
46
+ false
47
+ else
48
+ output = `identify -format "%w %h %[exif:Orientation]" #{stream.path} 2> /dev/null`
49
+ if $?.success? && output.present?
50
+ width, height, orientation = *output.scan(/(\d+) (\d+) (\d?)/).first.map(&:to_i)
51
+ rotated = (orientation & 0x4).nonzero?
52
+ @original_dimensions ||= rotated ? [height, width] : [width, height]
53
+ true
54
+ else
55
+ Bulldog.logger.warn "command failed (#{$?.exitstatus})"
56
+ @original_dimensions = [1, 1]
57
+ false
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,229 @@
1
+ module Bulldog
2
+ module Attachment
3
+ #
4
+ # Abstract base class of None and Base.
5
+ #
6
+ class Maybe
7
+ def initialize(record, name, stream)
8
+ @record = record
9
+ @name = name
10
+ @stream = stream
11
+ @value = stream && stream.target
12
+ @saved = value.is_a?(SavedFile)
13
+ end
14
+
15
+ attr_reader :record, :name, :stream, :value
16
+
17
+ #
18
+ # Return true if the original file for this attachment has been
19
+ # saved.
20
+ #
21
+ def saved?
22
+ @saved
23
+ end
24
+
25
+ #
26
+ # Set the saved flag.
27
+ #
28
+ attr_writer :saved
29
+
30
+ #
31
+ # Return the reflection for the attachment.
32
+ #
33
+ def reflection
34
+ @reflection ||= record.attachment_reflection_for(name)
35
+ end
36
+
37
+ #
38
+ # Return true if this object wraps the same IO, and is in the
39
+ # same state as the given Attachment.
40
+ #
41
+ def ==(other)
42
+ record == other.record &&
43
+ name == other.name &&
44
+ value == other.value &&
45
+ saved? == other.saved?
46
+ end
47
+
48
+ #
49
+ # Set the attachment type for this class.
50
+ #
51
+ # This will register it as the type of attachment to use for the
52
+ # given attachment type.
53
+ #
54
+ def self.handle(type)
55
+ self.attachment_type = type
56
+ Attachment.types_to_classes[type] = self
57
+ end
58
+
59
+ class_inheritable_accessor :attachment_type
60
+
61
+ #
62
+ # Return the class' attachment type.
63
+ #
64
+ def type
65
+ self.class.attachment_type
66
+ end
67
+
68
+ #
69
+ # Set the stored attributes in the record.
70
+ #
71
+ def set_stored_attributes
72
+ storable_attributes.each do |name, storable_attribute|
73
+ if (column_name = reflection.column_name_for_stored_attribute(name))
74
+ value = storable_attribute.value_for(self, :original)
75
+ record.send("#{column_name}=", value)
76
+ end
77
+ end
78
+ end
79
+
80
+ #
81
+ # Clear the stored attributes in the record.
82
+ #
83
+ def clear_stored_attributes
84
+ storable_attributes.each do |name, callback|
85
+ if (column_name = reflection.column_name_for_stored_attribute(name))
86
+ record.send("#{column_name}=", nil)
87
+ end
88
+ end
89
+ end
90
+
91
+ #
92
+ # Set the stored attributes in the attachment from the values in
93
+ # the record.
94
+ #
95
+ def read_storable_attributes
96
+ storable_attributes.each do |name, storable_attribute|
97
+ if (column_name = reflection.column_name_for_stored_attribute(name))
98
+ value = record.send(column_name)
99
+ value = send("deserialize_#{name}", value) if storable_attribute.cast
100
+ ivar = :"@#{name}"
101
+ if storable_attribute.per_style?
102
+ instance_variable_get(ivar) or
103
+ instance_variable_set(ivar, {})
104
+ instance_variable_get(ivar)[name] = value
105
+ else
106
+ instance_variable_set(ivar, value)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ #
113
+ # Return the path that the given style would be stored at.
114
+ #
115
+ # Unlike #path, this is not affected by whether or not the
116
+ # record is saved. It may depend on the attachment value,
117
+ # however, as some interpolations may be derived from the value
118
+ # assigned to the attachment (e.g., :extension).
119
+ #
120
+ def interpolate_path(style_name, params={})
121
+ template = reflection.path_template
122
+ style = reflection.styles[style_name]
123
+ params[:extension] ||= style[:format]
124
+ Interpolation.interpolate(template, record, name, style, params)
125
+ end
126
+
127
+ #
128
+ # Return the URL that the given style would be found at.
129
+ #
130
+ # Unlike #url, this is not affected by whether or not the record
131
+ # is saved. It may be depend on the attachment value, however,
132
+ # as some interpolations may be derived from the value assigned
133
+ # to the attachment (e.g., :extension).
134
+ #
135
+ def interpolate_url(style_name, params={})
136
+ template = reflection.url_template
137
+ style = reflection.styles[style_name]
138
+ params[:extension] ||= style[:format]
139
+ Interpolation.interpolate(template, record, name, style, params)
140
+ end
141
+
142
+ protected # ---------------------------------------------------
143
+
144
+ #
145
+ # Declare the given attribute as storable via
146
+ # Bulldog::Reflection::Configuration#store_attributes.
147
+ #
148
+ # Options:
149
+ #
150
+ # * <tt>:per_style</tt> - the attribute has a different value
151
+ # for each style. The access method takes a style name as an
152
+ # argument to select which one to return, and defaults to the
153
+ # attribute's default_style. Only :original is stored, and
154
+ # loaded after reading.
155
+ #
156
+ # * <tt>:memoize</tt> - memoize the value.
157
+ #
158
+ # * <tt>:cast</tt> - run the value through a serialize method
159
+ # before storing it in the database, and an unserialize
160
+ # method before initializing the attribute from the raw
161
+ # database value. The methods must be called
162
+ # #serialize_ATTRIBUTE and #unserialize_ATTRIBUTE.
163
+ #
164
+ def self.storable_attribute(name, options={}, &block)
165
+ params = {
166
+ :name => name,
167
+ :callback => block || name,
168
+ }.merge(options)
169
+ storable_attributes[name] = StorableAttribute.new(params)
170
+
171
+ if options[:memoize]
172
+ if options[:per_style]
173
+ class_eval <<-EOS, __FILE__, __LINE__+1
174
+ def #{name}_with_memoization(style_name=nil)
175
+ style_name ||= reflection.default_style
176
+ memoized_#{name}[style_name] ||= #{name}_without_memoization(style_name)
177
+ end
178
+ def memoized_#{name}
179
+ @memoized_#{name} ||= {}
180
+ end
181
+ EOS
182
+ else
183
+ class_eval <<-EOS, __FILE__, __LINE__+1
184
+ def #{name}_with_memoization
185
+ @#{name} ||= #{name}_without_memoization
186
+ end
187
+ EOS
188
+ end
189
+ alias_method_chain name, :memoization
190
+ end
191
+ end
192
+
193
+ #
194
+ # The list of storable attributes for this class.
195
+ #
196
+ class_inheritable_accessor :storable_attributes
197
+ self.storable_attributes = {}
198
+
199
+ class StorableAttribute
200
+ def initialize(attributes)
201
+ attributes.each do |name, value|
202
+ send("#{name}=", value)
203
+ end
204
+ end
205
+
206
+ def value_for(attachment, style_name)
207
+ value =
208
+ if callback.is_a?(Proc)
209
+ callback.call(attachment)
210
+ else
211
+ if per_style?
212
+ attachment.send(callback, style_name)
213
+ else
214
+ attachment.send(callback)
215
+ end
216
+ end
217
+ value = attachment.send("serialize_#{name}", value) if cast
218
+ value
219
+ end
220
+
221
+ attr_accessor :name, :callback, :per_style, :memoize, :cast
222
+
223
+ alias cast? cast
224
+ alias per_style? per_style
225
+ alias memoize? memoize
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,37 @@
1
+ module Bulldog
2
+ module Attachment
3
+ class None < Maybe
4
+ handle :none
5
+
6
+ #
7
+ # Return true. (Overrides ActiveSupport's Object#blank?)
8
+ #
9
+ # This means #present? will be false too.
10
+ #
11
+ def blank?
12
+ true
13
+ end
14
+
15
+ def path(style_name = reflection.default_style)
16
+ nil
17
+ end
18
+
19
+ def url(style_name = reflection.default_style)
20
+ nil
21
+ end
22
+
23
+ def size
24
+ nil
25
+ end
26
+
27
+ def save
28
+ end
29
+
30
+ def destroy
31
+ end
32
+
33
+ def process(event, *args)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,63 @@
1
+ module Bulldog
2
+ module Attachment
3
+ class Pdf < Base
4
+ handle :pdf
5
+
6
+ #
7
+ # Return the width and height of the named style, as a 2-element
8
+ # array.
9
+ #
10
+ # For :original, this is based on the output of ImageMagick's
11
+ # <tt>identify</tt> command. Other styles are calculated from
12
+ # the original style's dimensions, plus the style's :size and
13
+ # :filled attributes.
14
+ #
15
+ # +style_name+ defaults to the attribute's #default_style.
16
+ #
17
+ def dimensions(style_name)
18
+ if style_name.equal?(:original)
19
+ examine
20
+ @original_dimensions
21
+ else
22
+ style = reflection.styles[style_name]
23
+ target_dimensions = style[:size].split(/x/).map(&:to_i)
24
+ resized_dimensions(dimensions(:original), target_dimensions, style[:filled])
25
+ end
26
+ end
27
+
28
+ include HasDimensions
29
+
30
+ protected # ---------------------------------------------------
31
+
32
+ #
33
+ # Return the default processor class to use for this attachment.
34
+ #
35
+ def default_processor_type
36
+ :image_magick
37
+ end
38
+
39
+ #
40
+ # Read the original image metadata with ImageMagick's identify
41
+ # command.
42
+ #
43
+ def run_examination
44
+ if stream.missing?
45
+ @original_dimensions = [1, 1]
46
+ false
47
+ else
48
+ output = `identify -format "%w %h %[exif:Orientation]" #{stream.path}[0] 2> /dev/null`
49
+ if $?.success? && output.present?
50
+ width, height, orientation = *output.scan(/(\d+) (\d+) (\d?)/).first.map(&:to_i)
51
+ rotated = (orientation & 0x4).nonzero?
52
+ @original_dimensions ||= rotated ? [height, width] : [width, height]
53
+ true
54
+ else
55
+ Bulldog.logger.warn "command failed (#{$?.exitstatus})"
56
+ @original_dimensions = [1, 1]
57
+ false
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end