bulldog 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/DESCRIPTION.txt +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/bulldog.gemspec +157 -0
- data/lib/bulldog.rb +95 -0
- data/lib/bulldog/attachment.rb +49 -0
- data/lib/bulldog/attachment/base.rb +167 -0
- data/lib/bulldog/attachment/has_dimensions.rb +94 -0
- data/lib/bulldog/attachment/image.rb +63 -0
- data/lib/bulldog/attachment/maybe.rb +229 -0
- data/lib/bulldog/attachment/none.rb +37 -0
- data/lib/bulldog/attachment/pdf.rb +63 -0
- data/lib/bulldog/attachment/unknown.rb +11 -0
- data/lib/bulldog/attachment/video.rb +143 -0
- data/lib/bulldog/error.rb +5 -0
- data/lib/bulldog/has_attachment.rb +214 -0
- data/lib/bulldog/interpolation.rb +73 -0
- data/lib/bulldog/missing_file.rb +12 -0
- data/lib/bulldog/processor.rb +5 -0
- data/lib/bulldog/processor/argument_tree.rb +116 -0
- data/lib/bulldog/processor/base.rb +124 -0
- data/lib/bulldog/processor/ffmpeg.rb +172 -0
- data/lib/bulldog/processor/image_magick.rb +134 -0
- data/lib/bulldog/processor/one_shot.rb +19 -0
- data/lib/bulldog/reflection.rb +234 -0
- data/lib/bulldog/saved_file.rb +19 -0
- data/lib/bulldog/stream.rb +186 -0
- data/lib/bulldog/style.rb +38 -0
- data/lib/bulldog/style_set.rb +101 -0
- data/lib/bulldog/tempfile.rb +28 -0
- data/lib/bulldog/util.rb +92 -0
- data/lib/bulldog/validations.rb +68 -0
- data/lib/bulldog/vector2.rb +18 -0
- data/rails/init.rb +9 -0
- data/script/console +8 -0
- data/spec/data/empty.txt +0 -0
- data/spec/data/test.jpg +0 -0
- data/spec/data/test.mov +0 -0
- data/spec/data/test.pdf +0 -0
- data/spec/data/test.png +0 -0
- data/spec/data/test2.jpg +0 -0
- data/spec/helpers/image_creation.rb +8 -0
- data/spec/helpers/temporary_directory.rb +25 -0
- data/spec/helpers/temporary_models.rb +76 -0
- data/spec/helpers/temporary_values.rb +102 -0
- data/spec/helpers/test_upload_files.rb +108 -0
- data/spec/helpers/time_travel.rb +20 -0
- data/spec/integration/data/test.jpg +0 -0
- data/spec/integration/lifecycle_hooks_spec.rb +213 -0
- data/spec/integration/processing_image_attachments.rb +72 -0
- data/spec/integration/processing_video_attachments_spec.rb +82 -0
- data/spec/integration/saving_an_attachment_spec.rb +31 -0
- data/spec/matchers/file_operations.rb +159 -0
- data/spec/spec_helper.rb +76 -0
- data/spec/unit/attachment/base_spec.rb +311 -0
- data/spec/unit/attachment/image_spec.rb +128 -0
- data/spec/unit/attachment/maybe_spec.rb +126 -0
- data/spec/unit/attachment/pdf_spec.rb +137 -0
- data/spec/unit/attachment/video_spec.rb +176 -0
- data/spec/unit/attachment_spec.rb +61 -0
- data/spec/unit/has_attachment_spec.rb +700 -0
- data/spec/unit/interpolation_spec.rb +108 -0
- data/spec/unit/processor/argument_tree_spec.rb +159 -0
- data/spec/unit/processor/ffmpeg_spec.rb +467 -0
- data/spec/unit/processor/image_magick_spec.rb +260 -0
- data/spec/unit/processor/one_shot_spec.rb +70 -0
- data/spec/unit/reflection_spec.rb +338 -0
- data/spec/unit/stream_spec.rb +234 -0
- data/spec/unit/style_set_spec.rb +44 -0
- data/spec/unit/style_spec.rb +51 -0
- data/spec/unit/validations_spec.rb +491 -0
- data/spec/unit/vector2_spec.rb +27 -0
- data/tasks/bulldog_tasks.rake +4 -0
- 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
|