image_resizer 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.rspec +1 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +45 -0
  5. data/LICENSE +22 -0
  6. data/README.md +56 -0
  7. data/Rakefile +53 -0
  8. data/VERSION +1 -0
  9. data/image_resizer.gemspec +93 -0
  10. data/lib/image_resizer/analyzer.rb +51 -0
  11. data/lib/image_resizer/configurable.rb +206 -0
  12. data/lib/image_resizer/encoder.rb +55 -0
  13. data/lib/image_resizer/has_filename.rb +24 -0
  14. data/lib/image_resizer/loggable.rb +28 -0
  15. data/lib/image_resizer/processor.rb +220 -0
  16. data/lib/image_resizer/shell.rb +48 -0
  17. data/lib/image_resizer/temp_object.rb +216 -0
  18. data/lib/image_resizer/utils.rb +44 -0
  19. data/lib/image_resizer.rb +10 -0
  20. data/samples/DSC02119.JPG +0 -0
  21. data/samples/a.jp2 +0 -0
  22. data/samples/beach.jpg +0 -0
  23. data/samples/beach.png +0 -0
  24. data/samples/egg.png +0 -0
  25. data/samples/landscape.png +0 -0
  26. data/samples/round.gif +0 -0
  27. data/samples/sample.docx +0 -0
  28. data/samples/taj.jpg +0 -0
  29. data/samples/white pixel.png +0 -0
  30. data/spec/image_resizer/analyzer_spec.rb +78 -0
  31. data/spec/image_resizer/configurable_spec.rb +479 -0
  32. data/spec/image_resizer/encoder_spec.rb +41 -0
  33. data/spec/image_resizer/has_filename_spec.rb +88 -0
  34. data/spec/image_resizer/loggable_spec.rb +80 -0
  35. data/spec/image_resizer/processor_spec.rb +590 -0
  36. data/spec/image_resizer/shell_spec.rb +34 -0
  37. data/spec/image_resizer/temp_object_spec.rb +442 -0
  38. data/spec/spec_helper.rb +61 -0
  39. data/spec/support/argument_matchers.rb +19 -0
  40. data/spec/support/image_matchers.rb +58 -0
  41. data/spec/support/simple_matchers.rb +53 -0
  42. data/tmp/test_file +1 -0
  43. metadata +147 -0
@@ -0,0 +1,220 @@
1
+ module ImageResizer
2
+ class Processor
3
+
4
+ GRAVITIES = {
5
+ 'nw' => 'NorthWest',
6
+ 'n' => 'North',
7
+ 'ne' => 'NorthEast',
8
+ 'w' => 'West',
9
+ 'c' => 'Center',
10
+ 'e' => 'East',
11
+ 'sw' => 'SouthWest',
12
+ 's' => 'South',
13
+ 'se' => 'SouthEast'
14
+ }
15
+
16
+ # Geometry string patterns
17
+ RESIZE_GEOMETRY = /^\d*x\d*[><%^!]?$|^\d+@$/ # e.g. '300x200!'
18
+ CROPPED_RESIZE_GEOMETRY = /^(\d+)x(\d+)#(\w{1,2})?$/ # e.g. '20x50#ne'
19
+ CROP_GEOMETRY = /^(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?$/ # e.g. '30x30+10+10'
20
+ THUMB_GEOMETRY = Regexp.union RESIZE_GEOMETRY, CROPPED_RESIZE_GEOMETRY, CROP_GEOMETRY
21
+
22
+ include Configurable
23
+ include Utils
24
+
25
+
26
+ def resize(temp_object, options={})
27
+ width = options[:width].to_i
28
+ height = options[:height].to_i
29
+
30
+ if height == 0 && width == 0
31
+ temp_object.file
32
+ elsif height == 0
33
+ _resize(temp_object, "#{width}x")
34
+ elsif width == 0
35
+ _resize(temp_object, "x#{height}")
36
+ else
37
+ if options[:crop_from_top_if_portrait]
38
+ analyzer = ImageResizer::Analyzer.new
39
+ center_of_gravity = analyzer.aspect_ratio(temp_object) >= 1 ? 'c' : 'n'
40
+ else
41
+ center_of_gravity = 'c'
42
+ end
43
+
44
+ resize_and_crop(temp_object, :width => width.to_i, :height => height.to_i, :gravity => center_of_gravity)
45
+ end
46
+ end
47
+
48
+ def _resize(temp_object, geometry)
49
+ convert(temp_object, "-resize #{geometry}")
50
+ end
51
+
52
+ def auto_orient(temp_object)
53
+ convert(temp_object, "-auto-orient")
54
+ end
55
+
56
+ def crop(temp_object, opts={})
57
+ width = opts[:width]
58
+ height = opts[:height]
59
+ gravity = GRAVITIES[opts[:gravity]]
60
+ x = "#{opts[:x] || 0}"
61
+ x = '+' + x unless x[/^[+-]/]
62
+ y = "#{opts[:y] || 0}"
63
+ y = '+' + y unless y[/^[+-]/]
64
+ repage = opts[:repage] == false ? '' : '+repage'
65
+
66
+ resize = opts[:resize] ? "-resize #{opts[:resize]} " : ''
67
+ gravity = gravity ? "-gravity #{gravity} " : ''
68
+
69
+ convert(temp_object, "#{resize}#{gravity}-crop #{width}x#{height}#{x}#{y} #{repage}")
70
+ end
71
+
72
+ def resize_and_crop_around_point(temp_object, options)
73
+ analyzer = ImageResizer::Analyzer.new
74
+
75
+ desired_width = options[:width].to_i
76
+ desired_height = options[:height].to_i
77
+ desired_ratio = desired_height > 0 ? desired_width.to_f / desired_height : 0
78
+
79
+ original_width = analyzer.width(temp_object)
80
+ original_height = analyzer.height(temp_object)
81
+ original_ratio = original_width.to_f / original_height
82
+
83
+ if desired_ratio > original_ratio
84
+ width = original_width
85
+ height = width / desired_ratio
86
+ else
87
+ height = original_height
88
+ width = height * desired_ratio
89
+ end
90
+
91
+ focus_x = options[:point][0] * original_width
92
+ focus_y = options[:point][1] * original_height
93
+
94
+ half_width = width * 0.5
95
+ half_height = height * 0.5
96
+
97
+ upper_left_x = [focus_x - half_width, 0].max
98
+ upper_left_y = [focus_y - half_height, 0].max
99
+
100
+ lower_right_x = upper_left_x + width
101
+ lower_right_y = upper_left_y + height
102
+
103
+ x_offset = [lower_right_x - original_width, 0].max
104
+ y_offset = [lower_right_y - original_height, 0].max
105
+
106
+ upper_left_x -= x_offset
107
+ upper_left_y -= y_offset
108
+
109
+ lower_right_x -= x_offset
110
+ lower_right_y -= y_offset
111
+
112
+ upper_left_x_percent = upper_left_x / original_width
113
+ upper_left_y_percent = upper_left_y / original_height
114
+
115
+ lower_right_x_percent = lower_right_x / original_width
116
+ lower_right_y_percent = lower_right_y / original_height
117
+
118
+ crop_to_frame_and_resize(temp_object,
119
+ :upper_left => [upper_left_x_percent, upper_left_y_percent],
120
+ :lower_right => [lower_right_x_percent, lower_right_y_percent],
121
+ :width => desired_width,
122
+ :height => desired_height
123
+ )
124
+ end
125
+
126
+
127
+ def crop_to_frame_and_resize(temp_object, options)
128
+ analyzer = ImageResizer::Analyzer.new
129
+
130
+ desired_width = options[:width].to_i
131
+ desired_height = options[:height].to_i
132
+
133
+ upper_left_x_percent = options[:upper_left].first
134
+ upper_left_y_percent = options[:upper_left].last
135
+
136
+ lower_right_x_percent = options[:lower_right].first
137
+ lower_right_y_percent = options[:lower_right].last
138
+
139
+ original_width = analyzer.width(temp_object)
140
+ original_height = analyzer.height(temp_object)
141
+
142
+ upper_left_x = (original_width * upper_left_x_percent).round
143
+ upper_left_y = (original_height * upper_left_y_percent).round
144
+ frame_width = (original_width * (lower_right_x_percent - upper_left_x_percent)).round
145
+ frame_height = (original_height * (lower_right_y_percent - upper_left_y_percent)).round
146
+
147
+ if desired_width == 0 && frame_height > 0
148
+ ratio = frame_width.to_f / frame_height
149
+ desired_width = (desired_height * ratio).round
150
+ end
151
+
152
+ if desired_height == 0 && frame_width > 0
153
+ ratio = frame_height.to_f / frame_width
154
+ desired_height = (desired_width * ratio).round
155
+ end
156
+
157
+
158
+ convert(temp_object, "-crop #{frame_width}x#{frame_height}+#{upper_left_x}+#{upper_left_y} -resize #{desired_width}x#{desired_height} +repage")
159
+ end
160
+
161
+ def flip(temp_object)
162
+ convert(temp_object, "-flip")
163
+ end
164
+
165
+ def flop(temp_object)
166
+ convert(temp_object, "-flop")
167
+ end
168
+
169
+ def greyscale(temp_object)
170
+ convert(temp_object, "-colorspace Gray")
171
+ end
172
+ alias grayscale greyscale
173
+
174
+ def resize_and_crop(temp_object, opts={})
175
+ if !opts[:width] && !opts[:height]
176
+ return temp_object
177
+ elsif !opts[:width] || !opts[:height]
178
+ attrs = identify(temp_object)
179
+ opts[:width] ||= attrs[:width]
180
+ opts[:height] ||= attrs[:height]
181
+ end
182
+
183
+ opts[:gravity] ||= 'c'
184
+
185
+ opts[:resize] = "#{opts[:width]}x#{opts[:height]}^^"
186
+ crop(temp_object, opts)
187
+ end
188
+
189
+ def rotate(temp_object, amount, opts={})
190
+ convert(temp_object, "-rotate #{amount}#{opts[:qualifier]}")
191
+ end
192
+
193
+ def strip(temp_object)
194
+ convert(temp_object, "-strip")
195
+ end
196
+
197
+ def thumb(temp_object, geometry)
198
+ case geometry
199
+ when RESIZE_GEOMETRY
200
+ resize(temp_object, geometry)
201
+ when CROPPED_RESIZE_GEOMETRY
202
+ resize_and_crop(temp_object, :width => $1, :height => $2, :gravity => $3)
203
+ when CROP_GEOMETRY
204
+ crop(temp_object,
205
+ :width => $1,
206
+ :height => $2,
207
+ :x => $3,
208
+ :y => $4,
209
+ :gravity => $5
210
+ )
211
+ else raise ArgumentError, "Didn't recognise the geometry string #{geometry}"
212
+ end
213
+ end
214
+
215
+ def convert(temp_object, args='', format=nil)
216
+ format ? [super, {:format => format.to_sym}] : super
217
+ end
218
+
219
+ end
220
+ end
@@ -0,0 +1,48 @@
1
+ require 'shellwords'
2
+
3
+ module ImageResizer
4
+ module Shell
5
+
6
+ include Configurable
7
+ configurable_attr :log_commands, false
8
+
9
+ # Exceptions
10
+ class CommandFailed < RuntimeError; end
11
+
12
+ def run(command, args="")
13
+ full_command = "#{command} #{escape_args(args)}"
14
+ log.debug("Running command: #{full_command}") if log_commands
15
+ begin
16
+ result = `#{full_command}`
17
+ rescue Errno::ENOENT
18
+ raise_shell_command_failed(full_command)
19
+ end
20
+ if $?.exitstatus == 1
21
+ throw :unable_to_handle
22
+ elsif !$?.success?
23
+ raise_shell_command_failed(full_command)
24
+ end
25
+ result
26
+ end
27
+
28
+ def raise_shell_command_failed(command)
29
+ raise CommandFailed, "Command failed (#{command}) with exit status #{$?.exitstatus}"
30
+ end
31
+
32
+ def escape_args(args)
33
+ args.shellsplit.map do |arg|
34
+ quote arg.gsub(/\\?'/, %q('\\\\''))
35
+ end.join(' ')
36
+ end
37
+
38
+ def quote(string)
39
+ q = running_on_windows? ? '"' : "'"
40
+ q + string + q
41
+ end
42
+
43
+ def running_on_windows?
44
+ ENV['OS'] && ENV['OS'].downcase == 'windows_nt'
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,216 @@
1
+ require 'stringio'
2
+ require 'tempfile'
3
+ require 'pathname'
4
+
5
+ module ImageResizer
6
+
7
+ # A TempObject is used for HOLDING DATA.
8
+ # It's the thing that is passed between the datastore, the processor and the encoder, and is useful
9
+ # for separating how the data was created and how it is later accessed.
10
+ #
11
+ # You can initialize it various ways:
12
+ #
13
+ # temp_object = ImageResizer::TempObject.new('this is the content') # with a String
14
+ # temp_object = ImageResizer::TempObject.new(Pathname.new('path/to/content')) # with a Pathname
15
+ # temp_object = ImageResizer::TempObject.new(File.new('path/to/content')) # with a File
16
+ # temp_object = ImageResizer::TempObject.new(some_tempfile) # with a Tempfile
17
+ # temp_object = ImageResizer::TempObject.new(some_other_temp_object) # with another TempObject
18
+ #
19
+ # However, no matter how it was initialized, you can always access the data a number of ways:
20
+ #
21
+ # temp_object.data # returns a data string
22
+ # temp_object.file # returns a file object holding the data
23
+ # temp_object.path # returns a path for the file
24
+ #
25
+ # The data/file are created lazily, something which you may wish to take advantage of.
26
+ #
27
+ # For example, if a TempObject is initialized with a file, and temp_object.data is never called, then
28
+ # the data string will never be loaded into memory.
29
+ #
30
+ # Conversely, if the TempObject is initialized with a data string, and neither temp_object.file nor temp_object.path
31
+ # are ever called, then the filesystem will never be hit.
32
+ #
33
+ class TempObject
34
+
35
+ include HasFilename
36
+
37
+ # Exceptions
38
+ class Closed < RuntimeError; end
39
+
40
+ # Instance Methods
41
+
42
+ def initialize(obj, meta={})
43
+ if obj.is_a? TempObject
44
+ @data = obj.get_data
45
+ @tempfile = obj.get_tempfile
46
+ @pathname = obj.get_pathname
47
+ elsif obj.is_a? String
48
+ @data = obj
49
+ elsif obj.is_a? Tempfile
50
+ @tempfile = obj
51
+ elsif obj.is_a? File
52
+ @pathname = Pathname.new(obj.path)
53
+ elsif obj.is_a? Pathname
54
+ @pathname = obj
55
+ elsif obj.respond_to?(:tempfile)
56
+ @tempfile = obj.tempfile
57
+ elsif obj.respond_to?(:path) # e.g. Rack::Test::UploadedFile
58
+ @pathname = Pathname.new(obj.path)
59
+ else
60
+ raise ArgumentError, "#{self.class.name} must be initialized with a String, a Pathname, a File, a Tempfile, another TempObject, something that responds to .tempfile, or something that responds to .path"
61
+ end
62
+
63
+ @tempfile.close if @tempfile
64
+
65
+ # Original filename
66
+ @original_filename = if obj.respond_to?(:original_filename)
67
+ obj.original_filename
68
+ elsif @pathname
69
+ @pathname.basename.to_s
70
+ end
71
+
72
+ # Meta
73
+ @meta = meta
74
+ @meta[:name] ||= @original_filename if @original_filename
75
+ end
76
+
77
+ attr_reader :original_filename
78
+ attr_accessor :meta
79
+
80
+ def name
81
+ meta[:name]
82
+ end
83
+
84
+ def name=(name)
85
+ meta[:name] = name
86
+ end
87
+
88
+ def data
89
+ raise Closed, "can't read data as TempObject has been closed" if closed?
90
+ @data ||= file{|f| f.read }
91
+ end
92
+
93
+ def tempfile
94
+ raise Closed, "can't read from tempfile as TempObject has been closed" if closed?
95
+ @tempfile ||= begin
96
+ case
97
+ when @data
98
+ @tempfile = new_tempfile(@data)
99
+ when @pathname
100
+ @tempfile = copy_to_tempfile(@pathname.expand_path)
101
+ end
102
+ @tempfile
103
+ end
104
+ end
105
+
106
+ def file(&block)
107
+ f = tempfile.open
108
+ tempfile.binmode
109
+ if block_given?
110
+ ret = yield f
111
+ tempfile.close unless tempfile.closed?
112
+ else
113
+ ret = f
114
+ end
115
+ ret
116
+ end
117
+
118
+ def path
119
+ @pathname ? @pathname.expand_path.to_s : tempfile.path
120
+ end
121
+
122
+ def size
123
+ @data ? @data.bytesize : File.size(path)
124
+ end
125
+
126
+ def each(&block)
127
+ to_io do |io|
128
+ while part = io.read(block_size)
129
+ yield part
130
+ end
131
+ end
132
+ end
133
+
134
+ def to_file(path, opts={})
135
+ mode = opts[:mode] || 0644
136
+ prepare_path(path) unless opts[:mkdirs] == false
137
+ if @data
138
+ File.open(path, 'wb', mode){|f| f.write(@data) }
139
+ else
140
+ FileUtils.cp(self.path, path)
141
+ File.chmod(mode, path)
142
+ end
143
+ File.new(path, 'rb')
144
+ end
145
+
146
+ def to_io(&block)
147
+ @data ? StringIO.open(@data, 'rb', &block) : file(&block)
148
+ end
149
+
150
+ def close
151
+ @tempfile.close! if @tempfile
152
+ @data = nil
153
+ @closed = true
154
+ end
155
+
156
+ def closed?
157
+ !!@closed
158
+ end
159
+
160
+ def inspect
161
+ content_string = case
162
+ when @data
163
+ data_string = size > 20 ? "#{@data[0..20]}..." : @data
164
+ "data=#{data_string.inspect}"
165
+ when @pathname then "pathname=#{@pathname.inspect}"
166
+ when @tempfile then "tempfile=#{@tempfile.inspect}"
167
+ end
168
+ "<#{self.class.name} #{content_string} >"
169
+ end
170
+
171
+ def unique_id
172
+ @unique_id ||= "#{object_id}#{rand(1000000)}"
173
+ end
174
+
175
+ protected
176
+
177
+ # We don't use normal accessors here because #data etc. do more than just return the instance var
178
+ def get_data
179
+ @data
180
+ end
181
+
182
+ def get_pathname
183
+ @pathname
184
+ end
185
+
186
+ def get_tempfile
187
+ @tempfile
188
+ end
189
+
190
+ private
191
+
192
+ def block_size
193
+ 8192
194
+ end
195
+
196
+ def copy_to_tempfile(path)
197
+ tempfile = new_tempfile
198
+ FileUtils.cp path, tempfile.path
199
+ tempfile
200
+ end
201
+
202
+ def new_tempfile(content=nil)
203
+ tempfile = ext ? Tempfile.new(['ImageResizer', ".#{ext}"]) : Tempfile.new('ImageResizer')
204
+ tempfile.binmode
205
+ tempfile.write(content) if content
206
+ tempfile.close
207
+ tempfile
208
+ end
209
+
210
+ def prepare_path(path)
211
+ dir = File.dirname(path)
212
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
213
+ end
214
+
215
+ end
216
+ end
@@ -0,0 +1,44 @@
1
+ require 'tempfile'
2
+
3
+ module ImageResizer
4
+ module Utils
5
+
6
+ include Shell
7
+ include Loggable
8
+ include Configurable
9
+ configurable_attr :convert_command, "convert"
10
+ configurable_attr :identify_command, "identify"
11
+
12
+ private
13
+
14
+ def convert(temp_object=nil, args='', format=nil)
15
+ tempfile = new_tempfile(format)
16
+ run convert_command, %(#{quote(temp_object.path) if temp_object} #{args} #{quote(tempfile.path)})
17
+ tempfile
18
+ end
19
+
20
+ def identify(temp_object)
21
+ # example of details string:
22
+ # myimage.png PNG 200x100 200x100+0+0 8-bit DirectClass 31.2kb
23
+ format, width, height, depth = raw_identify(temp_object).scan(/([A-Z0-9]+) (\d+)x(\d+) .+ (\d+)-bit/)[0]
24
+ {
25
+ :format => format.downcase.to_sym,
26
+ :width => width.to_i,
27
+ :height => height.to_i,
28
+ :depth => depth.to_i
29
+ }
30
+ end
31
+
32
+ def raw_identify(temp_object, args='')
33
+ run identify_command, "#{args} #{quote(temp_object.path)}"
34
+ end
35
+
36
+ def new_tempfile(ext=nil)
37
+ tempfile = ext ? Tempfile.new(['ImageResizer', ".#{ext}"]) : Tempfile.new('ImageResizer')
38
+ tempfile.binmode
39
+ tempfile.close
40
+ tempfile
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+
2
+ require 'image_resizer/has_filename'
3
+ require 'image_resizer/loggable'
4
+ require 'image_resizer/configurable'
5
+ require 'image_resizer/shell'
6
+ require 'image_resizer/utils'
7
+ require 'image_resizer/analyzer'
8
+ require 'image_resizer/encoder'
9
+ require 'image_resizer/processor'
10
+ require 'image_resizer/temp_object'
Binary file
data/samples/a.jp2 ADDED
Binary file
data/samples/beach.jpg ADDED
Binary file
data/samples/beach.png ADDED
Binary file
data/samples/egg.png ADDED
Binary file
Binary file
data/samples/round.gif ADDED
Binary file
Binary file
data/samples/taj.jpg ADDED
Binary file
Binary file
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe ImageResizer::Analyzer do
4
+
5
+ before(:each) do
6
+ @image = ImageResizer::TempObject.new(SAMPLES_DIR.join('beach.png'))
7
+ @analyzer = ImageResizer::Analyzer.new
8
+ end
9
+
10
+ it "should return the width" do
11
+ @analyzer.width(@image).should == 280
12
+ end
13
+
14
+ it "should return the height" do
15
+ @analyzer.height(@image).should == 355
16
+ end
17
+
18
+ it "should return the aspect ratio" do
19
+ @analyzer.aspect_ratio(@image).should == (280.0/355.0)
20
+ end
21
+
22
+ it "should say if it's portrait" do
23
+ @analyzer.portrait?(@image).should be_true
24
+ end
25
+
26
+ it "should say if it's landscape" do
27
+ @analyzer.landscape?(@image).should be_false
28
+ end
29
+
30
+ it "should return the number of colours" do
31
+ @analyzer.number_of_colours(@image).should == 34703
32
+ end
33
+
34
+ it "should return the depth" do
35
+ @analyzer.depth(@image).should == 8
36
+ end
37
+
38
+ it "should return the format" do
39
+ @analyzer.format(@image).should == :png
40
+ end
41
+
42
+ %w(width height aspect_ratio number_of_colours depth format portrait? landscape?).each do |meth|
43
+ it "should throw unable_to_handle in #{meth.inspect} if it's not an image file" do
44
+ suppressing_stderr do
45
+ temp_object = ImageResizer::TempObject.new('blah')
46
+ lambda{
47
+ @analyzer.send(meth, temp_object)
48
+ }.should throw_symbol(:unable_to_handle)
49
+ end
50
+ end
51
+ end
52
+
53
+ it "should say if it's an image" do
54
+ @analyzer.image?(@image).should == true
55
+ end
56
+
57
+ it "should say if it's not an image" do
58
+ suppressing_stderr do
59
+ @analyzer.image?(ImageResizer::TempObject.new('blah')).should == false
60
+ end
61
+ end
62
+
63
+ it "should work for images with spaces in the filename" do
64
+ image = ImageResizer::TempObject.new(SAMPLES_DIR.join('white pixel.png'))
65
+ @analyzer.width(image).should == 1
66
+ end
67
+
68
+ it "should work (width) for images with capital letter extensions" do
69
+ image = ImageResizer::TempObject.new(SAMPLES_DIR.join('DSC02119.JPG'))
70
+ @analyzer.width(image).should == 1
71
+ end
72
+
73
+ it "should work (width) for images with numbers in the format" do
74
+ image = ImageResizer::TempObject.new(SAMPLES_DIR.join('a.jp2'))
75
+ @analyzer.width(image).should == 1
76
+ end
77
+
78
+ end