bulldog 0.1.1 → 0.2.0

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