bulldog 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/lib/bulldog/attachment/base.rb +3 -6
- data/lib/bulldog/attachment/has_dimensions.rb +43 -34
- data/lib/bulldog/attachment/image.rb +5 -26
- data/lib/bulldog/attachment/pdf.rb +3 -28
- data/lib/bulldog/attachment/video.rb +48 -45
- data/lib/bulldog/style.rb +28 -1
- data/lib/bulldog/version.rb +1 -1
- data/spec/data/3-bytes.txt +1 -0
- data/spec/data/4-bytes.txt +1 -0
- data/spec/data/5-bytes.txt +1 -0
- data/spec/data/6-bytes.txt +1 -0
- data/spec/data/test-20x10.jpg +0 -0
- data/spec/data/test-20x10.pdf +0 -0
- data/spec/data/test-20x10x1.mov +0 -0
- data/spec/data/test-40x30.jpg +0 -0
- data/spec/data/test-40x30.pdf +0 -0
- data/spec/data/test-40x30x1.mov +0 -0
- data/spec/helpers/files.rb +123 -0
- data/spec/integration/lifecycle_hooks_spec.rb +1 -1
- data/spec/integration/processing_image_attachments.rb +1 -13
- data/spec/macros/attachment/has_dimensions_spec.rb +313 -0
- data/spec/spec_helper.rb +3 -4
- data/spec/unit/attachment/base_spec.rb +25 -45
- data/spec/unit/attachment/image_spec.rb +48 -171
- data/spec/unit/attachment/maybe_spec.rb +4 -12
- data/spec/unit/attachment/pdf_spec.rb +18 -136
- data/spec/unit/attachment/video_spec.rb +98 -170
- data/spec/unit/attachment_spec.rb +1 -1
- data/spec/unit/has_attachment_spec.rb +29 -26
- data/spec/unit/interpolation_spec.rb +2 -2
- data/spec/unit/processor/ffmpeg_spec.rb +3 -3
- data/spec/unit/processor/image_magick_spec.rb +1 -1
- data/spec/unit/processor/one_shot_spec.rb +1 -1
- data/spec/unit/stream_spec.rb +3 -3
- data/spec/unit/style_spec.rb +40 -0
- data/spec/unit/validations_spec.rb +33 -33
- metadata +28 -8
- data/spec/helpers/temporary_directory.rb +0 -25
- data/spec/helpers/test_upload_files.rb +0 -108
data/CHANGELOG
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
== 0.2.0 2010-07-14
|
2
|
+
|
3
|
+
* Remove dimensions and aspect ratio as a storable attributes. Store
|
4
|
+
width and height instead.
|
5
|
+
* Videos return 0 tracks if the file is missing (but still dimensions
|
6
|
+
of 2x2 so thumbnail sizes can be calculated).
|
7
|
+
|
1
8
|
== 0.1.1 2010-06-30
|
2
9
|
|
3
10
|
* Create output directory when recording frames if necessary.
|
@@ -143,13 +143,10 @@ module Bulldog
|
|
143
143
|
end
|
144
144
|
|
145
145
|
#
|
146
|
-
# Return the value of the given
|
147
|
-
#
|
146
|
+
# Return the value of the given instance variable, running a
|
147
|
+
# file examination first if necessary.
|
148
148
|
#
|
149
|
-
|
150
|
-
#
|
151
|
-
def from_examination(name)
|
152
|
-
ivar = :"@#{name}"
|
149
|
+
def from_examination(ivar)
|
153
150
|
value = instance_variable_get(ivar) and
|
154
151
|
return value
|
155
152
|
examine
|
@@ -10,10 +10,8 @@ module Bulldog
|
|
10
10
|
def self.included(base)
|
11
11
|
super
|
12
12
|
base.class_eval do
|
13
|
-
storable_attribute :width
|
14
|
-
storable_attribute :height
|
15
|
-
storable_attribute :aspect_ratio, :per_style => true, :memoize => true
|
16
|
-
storable_attribute :dimensions , :per_style => true, :memoize => true, :cast => true
|
13
|
+
storable_attribute :width , :per_style => true, :memoize => true
|
14
|
+
storable_attribute :height, :per_style => true, :memoize => true
|
17
15
|
end
|
18
16
|
end
|
19
17
|
|
@@ -22,8 +20,13 @@ module Bulldog
|
|
22
20
|
#
|
23
21
|
# +style_name+ defaults to the attribute's #default_style.
|
24
22
|
#
|
25
|
-
def width(style_name)
|
26
|
-
|
23
|
+
def width(style_name=nil)
|
24
|
+
style_name ||= reflection.default_style
|
25
|
+
if style_name.equal?(:original)
|
26
|
+
from_examination :@original_width
|
27
|
+
else
|
28
|
+
dimensions(style_name).at(0)
|
29
|
+
end
|
27
30
|
end
|
28
31
|
|
29
32
|
#
|
@@ -31,8 +34,13 @@ module Bulldog
|
|
31
34
|
#
|
32
35
|
# +style_name+ defaults to the attribute's #default_style.
|
33
36
|
#
|
34
|
-
def height(style_name)
|
35
|
-
|
37
|
+
def height(style_name=nil)
|
38
|
+
style_name ||= reflection.default_style
|
39
|
+
if style_name.equal?(:original)
|
40
|
+
from_examination :@original_height
|
41
|
+
else
|
42
|
+
dimensions(style_name).at(1)
|
43
|
+
end
|
36
44
|
end
|
37
45
|
|
38
46
|
#
|
@@ -40,55 +48,56 @@ module Bulldog
|
|
40
48
|
#
|
41
49
|
# +style_name+ defaults to the attribute's #default_style.
|
42
50
|
#
|
43
|
-
def aspect_ratio(style_name)
|
44
|
-
|
51
|
+
def aspect_ratio(style_name=nil)
|
52
|
+
style_name ||= reflection.default_style
|
53
|
+
if style_name.equal?(:original)
|
54
|
+
original_width = from_examination(:@original_width)
|
55
|
+
original_height = from_examination(:@original_height)
|
56
|
+
original_width.to_f / original_height
|
57
|
+
else
|
58
|
+
w, h = *dimensions(style_name)
|
59
|
+
w.to_f / h
|
60
|
+
end
|
45
61
|
end
|
46
62
|
|
47
63
|
#
|
48
64
|
# Return the width and height of the named style, as a 2-element
|
49
65
|
# array.
|
50
66
|
#
|
51
|
-
def dimensions
|
52
|
-
|
67
|
+
def dimensions(style_name=nil)
|
68
|
+
style_name ||= reflection.default_style
|
69
|
+
if style_name.equal?(:original)
|
70
|
+
original_width = from_examination(:@original_width)
|
71
|
+
original_height = from_examination(:@original_height)
|
72
|
+
[original_width, original_height]
|
73
|
+
else
|
74
|
+
resize_dimensions(dimensions(:original), reflection.styles[style_name])
|
75
|
+
end
|
53
76
|
end
|
54
77
|
|
55
78
|
protected # -----------------------------------------------------
|
56
79
|
|
57
80
|
#
|
58
81
|
# Return the dimensions, as an array [width, height], that
|
59
|
-
# result from resizing +original_dimensions+
|
60
|
-
# +
|
61
|
-
# will fill the target box. Otherwise the aspect ratio will be
|
62
|
-
# maintained.
|
82
|
+
# result from resizing +original_dimensions+ for the given
|
83
|
+
# +style+.
|
63
84
|
#
|
64
|
-
def
|
65
|
-
if
|
66
|
-
|
85
|
+
def resize_dimensions(original_dimensions, style)
|
86
|
+
if style.filled?
|
87
|
+
style.dimensions
|
67
88
|
else
|
68
89
|
original_aspect_ratio = original_dimensions[0].to_f / original_dimensions[1]
|
69
|
-
target_aspect_ratio =
|
90
|
+
target_aspect_ratio = style.dimensions[0].to_f / style.dimensions[1]
|
70
91
|
if original_aspect_ratio > target_aspect_ratio
|
71
|
-
width =
|
92
|
+
width = style.dimensions[0]
|
72
93
|
height = (width / original_aspect_ratio).round
|
73
94
|
else
|
74
|
-
height =
|
95
|
+
height = style.dimensions[1]
|
75
96
|
width = (height * original_aspect_ratio).round
|
76
97
|
end
|
77
98
|
[width, height]
|
78
99
|
end
|
79
100
|
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{|s| s.to_i}
|
91
|
-
end
|
92
101
|
end
|
93
102
|
end
|
94
103
|
end
|
@@ -2,33 +2,11 @@ module Bulldog
|
|
2
2
|
module Attachment
|
3
3
|
class Image < Base
|
4
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
|
-
from_examination :original_dimensions
|
20
|
-
else
|
21
|
-
style = reflection.styles[style_name]
|
22
|
-
target_dimensions = style[:size].split(/x/).map{|s| s.to_i}
|
23
|
-
resized_dimensions(dimensions(:original), target_dimensions, style[:filled])
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
5
|
include HasDimensions
|
28
6
|
|
29
7
|
def unload
|
30
8
|
super
|
31
|
-
@
|
9
|
+
@original_width = @original_height = nil
|
32
10
|
end
|
33
11
|
|
34
12
|
protected # ---------------------------------------------------
|
@@ -46,18 +24,19 @@ module Bulldog
|
|
46
24
|
#
|
47
25
|
def run_examination
|
48
26
|
if stream.missing?
|
49
|
-
@
|
27
|
+
@original_width, @original_height = 1, 1
|
50
28
|
false
|
51
29
|
else
|
52
30
|
output = `identify -format "%w %h %[exif:Orientation]" #{stream.path} 2> /dev/null`
|
53
31
|
if $?.success? && output.present?
|
54
32
|
width, height, orientation = *output.scan(/(\d+) (\d+) (\d?)/).first.map{|s| s.to_i}
|
55
33
|
rotated = (5..8).include?(orientation)
|
56
|
-
@
|
34
|
+
@original_width = rotated ? height : width
|
35
|
+
@original_height = rotated ? width : height
|
57
36
|
true
|
58
37
|
else
|
59
38
|
Bulldog.logger.warn "command failed (#{$?.exitstatus})"
|
60
|
-
@
|
39
|
+
@original_width, @original_height = 1, 1
|
61
40
|
false
|
62
41
|
end
|
63
42
|
end
|
@@ -2,28 +2,6 @@ module Bulldog
|
|
2
2
|
module Attachment
|
3
3
|
class Pdf < Base
|
4
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
|
-
from_examination :original_dimensions
|
20
|
-
else
|
21
|
-
style = reflection.styles[style_name]
|
22
|
-
target_dimensions = style[:size].split(/x/).map{|s| s.to_i}
|
23
|
-
resized_dimensions(dimensions(:original), target_dimensions, style[:filled])
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
5
|
include HasDimensions
|
28
6
|
|
29
7
|
protected # ---------------------------------------------------
|
@@ -41,18 +19,15 @@ module Bulldog
|
|
41
19
|
#
|
42
20
|
def run_examination
|
43
21
|
if stream.missing?
|
44
|
-
@
|
22
|
+
@original_width, @original_height = 1, 1
|
45
23
|
false
|
46
24
|
else
|
47
|
-
output = `identify -format "%w %h
|
25
|
+
output = `identify -format "%w %h" #{stream.path}[0] 2> /dev/null`
|
48
26
|
if $?.success? && output.present?
|
49
|
-
|
50
|
-
rotated = (orientation & 0x4).nonzero?
|
51
|
-
@original_dimensions ||= rotated ? [height, width] : [width, height]
|
27
|
+
@original_width, @original_height = *output.scan(/(\d+) (\d+)/).first.map{|s| s.to_i}
|
52
28
|
true
|
53
29
|
else
|
54
30
|
Bulldog.logger.warn "command failed (#{$?.exitstatus})"
|
55
|
-
@original_dimensions = [1, 1]
|
56
31
|
false
|
57
32
|
end
|
58
33
|
end
|
@@ -2,19 +2,6 @@ module Bulldog
|
|
2
2
|
module Attachment
|
3
3
|
class Video < Base
|
4
4
|
handle :video
|
5
|
-
|
6
|
-
#
|
7
|
-
# Return the width and height of the named style, as a 2-element
|
8
|
-
# array.
|
9
|
-
#
|
10
|
-
# This runs ffmpeg for, and only for, the original style.
|
11
|
-
#
|
12
|
-
# +style_name+ defaults to the attribute's #default_style.
|
13
|
-
#
|
14
|
-
def dimensions(style_name)
|
15
|
-
video_tracks(style_name).first.dimensions
|
16
|
-
end
|
17
|
-
|
18
5
|
include HasDimensions
|
19
6
|
|
20
7
|
#
|
@@ -26,12 +13,7 @@ module Bulldog
|
|
26
13
|
# +style_name+ defaults to the attribute's #default_style.
|
27
14
|
#
|
28
15
|
def duration(style_name)
|
29
|
-
|
30
|
-
if stream.missing?
|
31
|
-
0
|
32
|
-
else
|
33
|
-
from_examination :original_duration
|
34
|
-
end
|
16
|
+
from_examination :@original_duration
|
35
17
|
end
|
36
18
|
|
37
19
|
#
|
@@ -40,27 +22,18 @@ module Bulldog
|
|
40
22
|
#
|
41
23
|
# Each VideoTrack has:
|
42
24
|
#
|
43
|
-
# * <tt>#
|
44
|
-
#
|
25
|
+
# * <tt>#duration</tt> - the duration of the video track.
|
26
|
+
# * <tt>#dimensions</tt> - the [width, height] of the video
|
27
|
+
# track.
|
45
28
|
#
|
46
29
|
def video_tracks(style_name=nil)
|
47
30
|
style_name ||= reflection.default_style
|
48
|
-
if style_name
|
49
|
-
|
50
|
-
[VideoTrack.new(:dimensions => [2, 2])]
|
51
|
-
else
|
52
|
-
examine unless @original_video_tracks
|
53
|
-
if @original_video_tracks.empty?
|
54
|
-
@original_video_tracks << VideoTrack.new(:dimensions => [2, 2])
|
55
|
-
end
|
56
|
-
@original_video_tracks
|
57
|
-
end
|
31
|
+
if style_name.equal?(:original)
|
32
|
+
from_examination :@original_video_tracks
|
58
33
|
else
|
59
34
|
style = reflection.styles[style_name]
|
60
|
-
target_dimensions = style[:size].split(/x/).map{|s| s.to_i}
|
61
35
|
video_tracks(:original).map do |video_track|
|
62
|
-
dimensions =
|
63
|
-
dimensions.map!{|i| i &= -2} # some codecs require multiples of 2
|
36
|
+
dimensions = resize_dimensions(dimensions(:original), style)
|
64
37
|
VideoTrack.new(:dimensions => dimensions)
|
65
38
|
end
|
66
39
|
end
|
@@ -72,12 +45,12 @@ module Bulldog
|
|
72
45
|
#
|
73
46
|
# AudioTrack objects do not yet have any useful methods.
|
74
47
|
#
|
75
|
-
def audio_tracks(style_name)
|
48
|
+
def audio_tracks(style_name=nil)
|
76
49
|
examine
|
77
50
|
@original_audio_tracks
|
78
51
|
end
|
79
52
|
|
80
|
-
storable_attribute :duration
|
53
|
+
storable_attribute :duration, :per_style => true, :memoize => true
|
81
54
|
|
82
55
|
def unload
|
83
56
|
super
|
@@ -95,38 +68,66 @@ module Bulldog
|
|
95
68
|
:ffmpeg
|
96
69
|
end
|
97
70
|
|
71
|
+
#
|
72
|
+
# Overridden to round down to multiples of 2, as required by
|
73
|
+
# some codecs.
|
74
|
+
#
|
75
|
+
def resize_dimensions(original_dimensions, style)
|
76
|
+
dimensions = super
|
77
|
+
dimensions.map!{|i| i &= -2}
|
78
|
+
end
|
79
|
+
|
98
80
|
private # -----------------------------------------------------
|
99
81
|
|
100
82
|
#
|
101
83
|
# Read the original image metadata with ffmpeg.
|
102
84
|
#
|
103
85
|
def run_examination
|
104
|
-
|
105
|
-
|
106
|
-
|
86
|
+
if stream.missing?
|
87
|
+
set_defaults
|
88
|
+
false
|
89
|
+
else
|
90
|
+
output = `ffmpeg -i #{stream.path} 2>&1`
|
91
|
+
# ffmpeg exits nonzero - don't bother checking status.
|
92
|
+
parse_output(output)
|
93
|
+
true
|
94
|
+
end
|
107
95
|
end
|
108
96
|
|
109
|
-
def
|
110
|
-
result = false
|
97
|
+
def set_defaults
|
111
98
|
@original_duration = 0
|
112
|
-
@
|
99
|
+
@original_width = 2
|
100
|
+
@original_height = 2
|
113
101
|
@original_audio_tracks = []
|
102
|
+
@original_video_tracks = []
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_output(output)
|
106
|
+
result = false
|
107
|
+
set_defaults
|
114
108
|
io = StringIO.new(output)
|
115
109
|
while (line = io.gets)
|
116
110
|
case line
|
117
111
|
when /^Input #0, (.*?), from '(?:.*)':$/
|
118
112
|
result = true
|
113
|
+
duration = nil
|
119
114
|
when /^ Duration: (\d+):(\d+):(\d+)\.(\d+)/
|
120
|
-
|
115
|
+
duration = $1.to_i.hours + $2.to_i.minutes + $3.to_i.seconds
|
121
116
|
when /Stream #(?:.*?): Video: /
|
122
117
|
if $' =~ /(\d+)x(\d+)/
|
123
118
|
dimensions = [$1.to_i, $2.to_i]
|
124
119
|
end
|
125
|
-
@original_video_tracks << VideoTrack.new(:dimensions => dimensions)
|
120
|
+
@original_video_tracks << VideoTrack.new(:dimensions => dimensions, :duration => duration)
|
126
121
|
when /Stream #(?:.*?): Audio: (.*?)/
|
127
|
-
@original_audio_tracks << AudioTrack.new
|
122
|
+
@original_audio_tracks << AudioTrack.new(:duration => duration)
|
128
123
|
end
|
129
124
|
end
|
125
|
+
if (track = @original_video_tracks.first)
|
126
|
+
@original_width, @original_height = *track.dimensions
|
127
|
+
end
|
128
|
+
if (track = @original_video_tracks.first || @original_audio_tracks.first)
|
129
|
+
@original_duration = track.duration
|
130
|
+
end
|
130
131
|
result
|
131
132
|
end
|
132
133
|
|
@@ -136,6 +137,8 @@ module Bulldog
|
|
136
137
|
send("#{name}=", value)
|
137
138
|
end
|
138
139
|
end
|
140
|
+
|
141
|
+
attr_accessor :duration
|
139
142
|
end
|
140
143
|
|
141
144
|
class VideoTrack < Track
|
data/lib/bulldog/style.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
module Bulldog
|
2
|
+
#
|
3
|
+
# Represents a style to generate.
|
4
|
+
#
|
2
5
|
class Style
|
3
6
|
def initialize(name, attributes={})
|
4
7
|
@name = name
|
5
8
|
@attributes = attributes
|
9
|
+
set_dimensions(attributes[:size])
|
6
10
|
end
|
7
11
|
|
8
12
|
attr_reader :name, :attributes
|
@@ -15,7 +19,12 @@ module Bulldog
|
|
15
19
|
#
|
16
20
|
# Set the value of the given style attribute.
|
17
21
|
#
|
18
|
-
|
22
|
+
def []=(name, value)
|
23
|
+
if name == :size
|
24
|
+
set_dimensions(value)
|
25
|
+
end
|
26
|
+
attributes[name] = value
|
27
|
+
end
|
19
28
|
|
20
29
|
#
|
21
30
|
# Return true if the argument is a Style with the same name and
|
@@ -33,6 +42,24 @@ module Bulldog
|
|
33
42
|
|
34
43
|
delegate :hash, :eql?, :to => :name
|
35
44
|
|
45
|
+
#
|
46
|
+
# The [width, height] specified by :size, or nil if there is no :size.
|
47
|
+
#
|
48
|
+
attr_reader :dimensions
|
49
|
+
|
50
|
+
#
|
51
|
+
# Return true if :filled is true, false otherwise.
|
52
|
+
#
|
53
|
+
def filled?
|
54
|
+
!!self[:filled]
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def set_dimensions(value)
|
60
|
+
@dimensions = value ? value.scan(/\A(\d+)x(\d+)\z/).first.map{|s| s.to_i} : nil
|
61
|
+
end
|
62
|
+
|
36
63
|
ORIGINAL = new(:original, {})
|
37
64
|
end
|
38
65
|
end
|