bulldog 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG +7 -0
  2. data/lib/bulldog/attachment/base.rb +3 -6
  3. data/lib/bulldog/attachment/has_dimensions.rb +43 -34
  4. data/lib/bulldog/attachment/image.rb +5 -26
  5. data/lib/bulldog/attachment/pdf.rb +3 -28
  6. data/lib/bulldog/attachment/video.rb +48 -45
  7. data/lib/bulldog/style.rb +28 -1
  8. data/lib/bulldog/version.rb +1 -1
  9. data/spec/data/3-bytes.txt +1 -0
  10. data/spec/data/4-bytes.txt +1 -0
  11. data/spec/data/5-bytes.txt +1 -0
  12. data/spec/data/6-bytes.txt +1 -0
  13. data/spec/data/test-20x10.jpg +0 -0
  14. data/spec/data/test-20x10.pdf +0 -0
  15. data/spec/data/test-20x10x1.mov +0 -0
  16. data/spec/data/test-40x30.jpg +0 -0
  17. data/spec/data/test-40x30.pdf +0 -0
  18. data/spec/data/test-40x30x1.mov +0 -0
  19. data/spec/helpers/files.rb +123 -0
  20. data/spec/integration/lifecycle_hooks_spec.rb +1 -1
  21. data/spec/integration/processing_image_attachments.rb +1 -13
  22. data/spec/macros/attachment/has_dimensions_spec.rb +313 -0
  23. data/spec/spec_helper.rb +3 -4
  24. data/spec/unit/attachment/base_spec.rb +25 -45
  25. data/spec/unit/attachment/image_spec.rb +48 -171
  26. data/spec/unit/attachment/maybe_spec.rb +4 -12
  27. data/spec/unit/attachment/pdf_spec.rb +18 -136
  28. data/spec/unit/attachment/video_spec.rb +98 -170
  29. data/spec/unit/attachment_spec.rb +1 -1
  30. data/spec/unit/has_attachment_spec.rb +29 -26
  31. data/spec/unit/interpolation_spec.rb +2 -2
  32. data/spec/unit/processor/ffmpeg_spec.rb +3 -3
  33. data/spec/unit/processor/image_magick_spec.rb +1 -1
  34. data/spec/unit/processor/one_shot_spec.rb +1 -1
  35. data/spec/unit/stream_spec.rb +3 -3
  36. data/spec/unit/style_spec.rb +40 -0
  37. data/spec/unit/validations_spec.rb +33 -33
  38. metadata +28 -8
  39. data/spec/helpers/temporary_directory.rb +0 -25
  40. 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 attribute from an instance
147
- # variable set during file examination.
146
+ # Return the value of the given instance variable, running a
147
+ # file examination first if necessary.
148
148
  #
149
- # If not set, runs a file examination first.
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 , :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
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
- dimensions(style_name)[0]
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
- dimensions(style_name)[1]
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
- width(style_name).to_f / height(style_name)
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
- raise 'abstract method called'
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+ 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.
82
+ # result from resizing +original_dimensions+ for the given
83
+ # +style+.
63
84
  #
64
- def resized_dimensions(original_dimensions, target_dimensions, fill)
65
- if fill
66
- target_dimensions
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 = target_dimensions[0].to_f / target_dimensions[1]
90
+ target_aspect_ratio = style.dimensions[0].to_f / style.dimensions[1]
70
91
  if original_aspect_ratio > target_aspect_ratio
71
- width = target_dimensions[0]
92
+ width = style.dimensions[0]
72
93
  height = (width / original_aspect_ratio).round
73
94
  else
74
- height = target_dimensions[1]
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
- @original_dimensions = nil
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
- @original_dimensions = [1, 1]
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
- @original_dimensions = rotated ? [height, width] : [width, height]
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
- @original_dimensions = [1, 1]
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
- @original_dimensions = [1, 1]
22
+ @original_width, @original_height = 1, 1
45
23
  false
46
24
  else
47
- output = `identify -format "%w %h %[exif:Orientation]" #{stream.path}[0] 2> /dev/null`
25
+ output = `identify -format "%w %h" #{stream.path}[0] 2> /dev/null`
48
26
  if $?.success? && output.present?
49
- width, height, orientation = *output.scan(/(\d+) (\d+) (\d?)/).first.map{|s| s.to_i}
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
- # TODO: support styles with different durations
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>#dimension</tt> - the dimensions of the video track,
44
- # [width, height].
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 == :original
49
- if stream.missing?
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 = resized_dimensions(dimensions(:original), target_dimensions, style[:filled])
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 , :per_style => true, :memoize => true
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
- return false if stream.missing?
105
- output = `ffmpeg -i #{stream.path} 2>&1`
106
- parse_output(output)
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 parse_output(output)
110
- result = false
97
+ def set_defaults
111
98
  @original_duration = 0
112
- @original_video_tracks = []
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
- @original_duration = $1.to_i.hours + $2.to_i.minutes + $3.to_i.seconds
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
@@ -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
- delegate :[]=, :to => :attributes
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