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