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