image_proc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-01-09
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,15 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ init.rb
6
+ lib/image_proc.rb
7
+ test/input/horizontal.gif
8
+ test/input/horizontal.jpg
9
+ test/input/horizontal.png
10
+ test/input/vertical.gif
11
+ test/input/vertical.jpg
12
+ test/input/vertical.png
13
+ test/resize_test_helper.rb
14
+ test/test_geom.rb
15
+ test/test_image_proc.rb
@@ -0,0 +1,36 @@
1
+ = ImageProc
2
+
3
+ * http://wiretap.rubyforge.org/image_proc
4
+
5
+ == DESCRIPTION:
6
+
7
+ A no-frills image resizer, with pluggable backends. No extra software required on OS X
8
+
9
+ == INSTALL:
10
+
11
+ * sudo gem install image_proc
12
+
13
+ == LICENSE:
14
+
15
+ (The MIT License)
16
+
17
+ Copyright (c) 2009 Julik Tarkhanov <me@julik.nl>
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining
20
+ a copy of this software and associated documentation files (the
21
+ 'Software'), to deal in the Software without restriction, including
22
+ without limitation the rights to use, copy, modify, merge, publish,
23
+ distribute, sublicense, and/or sell copies of the Software, and to
24
+ permit persons to whom the Software is furnished to do so, subject to
25
+ the following conditions:
26
+
27
+ The above copyright notice and this permission notice shall be
28
+ included in all copies or substantial portions of the Software.
29
+
30
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
31
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
32
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
33
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
34
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
35
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
36
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/image_proc.rb'
4
+
5
+ Hoe.new('image_proc', ImageProc::VERSION) do |p|
6
+ p.description = 'A no-frills image metadata and resizing'
7
+ p.developer('Julik', 'me@julik.nl')
8
+ p.rubyforge_name = 'wiretap'
9
+ p.remote_rdoc_dir = 'image_proc'
10
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/lib/image_proc'
@@ -0,0 +1,293 @@
1
+ # A simplistic interface to shell-based image processing. Pluggable, compact and WIN32-incompatible by
2
+ # design. Sort of like the Processors in attachment_fu but less. Less.
3
+ #
4
+ # width, height = ImageProc.get_bounds("image.png")
5
+ # thumb_filename = ImageProc.resize("image.png", "thumb.png", "50x50")
6
+ #
7
+ # The whole idea is: a backend does not have to support cropping (we don't do it), it has only to be able to resize,
8
+ # and a backend should have 2 public methods. That's the game.
9
+ require 'open3'
10
+
11
+ class ImageProc
12
+ VERSION = '0.1.0'
13
+
14
+ class Error < RuntimeError; end
15
+ class MissingInput < Error; end
16
+ class NoDestinationDir < Error; end
17
+ class DestinationLocked < Error; end
18
+ class NoOverwrites < Error; end
19
+ class FormatUnsupported < Error; end
20
+ class InvalidOptions < Error; end
21
+
22
+ HARMLESS = []
23
+ class << self
24
+ # Assign a specific processor class
25
+ def engine=(kls); @@engine = kls; end
26
+
27
+ # Get the processor class currently assigned
28
+ def engine; @@engine ||= detect_engine; @@engine; end
29
+
30
+ # Tries to detect the best engine available
31
+ def detect_engine
32
+ if RUBY_PLATFORM =~ /darwin/i
33
+ ImageProcSips
34
+ elsif (`which convert` =~ /^\// )
35
+ ImageProcConvert
36
+ else
37
+ raise "This system has no image processing facitilites that we can use"
38
+ end
39
+ end
40
+
41
+ # Qukckly get bounds of an image
42
+ # ImageProc.get_bounds("/tmp/upload.tif") #=> [100, 120]
43
+ def get_bounds(of)
44
+ engine.new.get_bounds(File.expand_path(of))
45
+ end
46
+
47
+ def method_missing(*args) #:nodoc:
48
+ engine.new.send(*args)
49
+ end
50
+ end
51
+
52
+ # Deprecated - pass the fitting as geometry string. Will use proportional fitting.
53
+ def resize(from, to, geom)
54
+ to_width, to_height = geom.scan(/(\d+)/).flatten
55
+ resize_fit_both(from, to, to_width, to_height).shift
56
+ end
57
+ alias_method :process, :resize
58
+
59
+ # Resizes with specific options passed as a hash
60
+ # ImageProc.resize_with_options "/tmp/foo.jpg", "bla.jpg", :width => 120, :height => 30
61
+ def resize_with_options(from_path, to_path, opts = {})
62
+ # raise InvalidOptions,
63
+ # "The only allowed options are :width, :height and :fill" if (opts.keys - [:width, :height, :fill]).any?
64
+ raise InvalidOptions,
65
+ "Pass width, height or both" unless (opts.keys & [:width, :height]).any?
66
+ opts.each_pair { |k,v| raise InvalidOptions, "#{k.inspect} cannot be set to nil" if v.nil? }
67
+
68
+ if opts[:width] && opts[:height] && opts[:fill]
69
+ resize_fit_fill(from_path, to_path, opts[:width], opts[:height])
70
+ elsif opts[:width] && opts[:height]
71
+ resize_fit(from_path, to_path, opts[:width], opts[:height])
72
+ elsif opts[:width]
73
+ resize_fit_width(from_path, to_path, opts[:width])
74
+ elsif opts[:height]
75
+ resize_fit_height(from_path, to_path, opts[:height])
76
+ else
77
+ raise "This should never happen"
78
+ end
79
+ end
80
+
81
+ # Resize an image fitting the biggest side of it to the side of a square. A must for thumbs.
82
+ def resize_fit_square(from_path, to_path, square_side)
83
+ resize_fit_both(from_path, to_path, square_side, square_side)
84
+ end
85
+
86
+ # Resize an image fitting the boundary exactly. Will stretch and squash.
87
+ def resize_exact(from_path, to_path, to_width, to_height)
88
+ validate_input_output_files(from_path, to_path)
89
+ @target_w, @target_h = to_width, to_height
90
+ resetting_state_afterwards { process_exact }
91
+ end
92
+
93
+ # Resize an image fitting it into a rect.
94
+ def resize_fit_both(from_path, to_path, to_width, to_height)
95
+ validate_input_output_files(from_path, to_path)
96
+ @target_w, @target_h = fit_sizes(get_bounds(from_path), :width => to_width, :height => to_height)
97
+ resetting_state_afterwards { process_exact }
98
+ end
99
+ alias_method :resize_fit, :resize_fit_both
100
+
101
+ # Resize an image fitting the biggest side of it to the side of a square. A must for thumbs.
102
+ def resize_fit_width(from_path, to_path, width)
103
+ validate_input_output_files(from_path, to_path)
104
+
105
+ @target_w, @target_h = fit_sizes get_bounds(from_path), :width => width
106
+ resetting_state_afterwards { process_exact }
107
+ end
108
+
109
+ # Same as resize_fit_width
110
+ def resize_fit_height(from_path, to_path, height)
111
+ validate_input_output_files(from_path, to_path)
112
+ @target_w, @target_h = fit_sizes(get_bounds(from_path), :height => height)
113
+ resetting_state_afterwards { process_exact }
114
+ end
115
+
116
+ # Will resize the image so that it's part always fills the rect of +width+ and +height+
117
+ # It's recommended to then simply use CSS overflow to crop off the edges which are not necessary.
118
+ # If you want more involved processing calculate the geometry directly.
119
+ def resize_fit_fill(from_path, to_path, width, height)
120
+ validate_input_output_files(from_path, to_path)
121
+ @target_w, @target_h = fit_sizes_with_crop get_bounds(from_path), :height => height, :width => width
122
+ resetting_state_afterwards { process_exact }
123
+ end
124
+
125
+ # Will fit the passed array of [input_width, input_heitght] proportionally and return an array of
126
+ # [recommended_width, recommended_height] honoring the following parameters:
127
+ #
128
+ # :width - maximum width of the bounding rect
129
+ # :height - maximum height of the bounding rect
130
+ #
131
+ # If you pass both the bounds will be fit into the rect having the :width and :height proportionally, downsizing the
132
+ # bounds if necessary. Useful for calculating needed size before resizing.
133
+ def fit_sizes(bounds, opts)
134
+
135
+ disallow_nil_values_in(opts)
136
+ integerize_values_of(opts)
137
+
138
+ ratio = bounds[0].to_f / bounds[1].to_f
139
+ floats = case (opts.keys & [:height, :width])
140
+ when []
141
+ raise "The options #{opts.inspect} do not contain proper bounds"
142
+ when [:width]
143
+ desired_w = opts[:width]
144
+ [desired_w, desired_w / ratio]
145
+ when [:height]
146
+ desired_h = opts[:height]
147
+ [desired_h * ratio, desired_h]
148
+ else # both, use reduction
149
+ smallest_side = [opts[:width], opts[:height]].sort.shift
150
+ if bounds[0] > bounds[1] # horizontal
151
+ fit_sizes bounds, :width => smallest_side
152
+ else
153
+ fit_sizes bounds, :height => smallest_side
154
+ end
155
+ end
156
+
157
+ # Prevent zero results
158
+ prevent_zeroes_in(floats)
159
+
160
+ # Nudge output values to pixels so that we fit exactly
161
+ floats[0] = opts[:width] if (opts[:width] && floats[0] > opts[:width])
162
+ floats[1] = opts[:height] if (opts[:height] && floats[1] > opts[:height])
163
+ floats
164
+ end
165
+
166
+ # Will fit the passed array of [input_width, input_heitght] to fill the whole rect and return an array of
167
+ # [recommended_width, recommended_height] honoring the following parameters:
168
+ #
169
+ # :width - maximum width of the bounding rect
170
+ # :height - maximum height of the bounding rect
171
+ #
172
+ # In contrast to fit_sizes it requires BOTH.
173
+ #
174
+ # It's recommended to clip the image which will be created with these bounds using CSS, as not all resizers support
175
+ # cropping - and besides it's just too many vars.
176
+ def fit_sizes_with_crop(bounds, opts)
177
+ raise Error, "fit_sizes_with_crop requires both width and height" unless (opts.keys & [:width, :height]).length == 2
178
+ scale = [opts[:width].to_f / bounds[0], opts[:height].to_f / bounds[1]].sort.pop
179
+ result = [bounds[0] * scale, bounds[1] * scale]
180
+ result.map{|e| e.round}
181
+ end
182
+
183
+ private
184
+ def prevent_zeroes_in(floats)
185
+ floats.map!{|f| r = f.round.to_i; (r.zero? ? 1 : r) }
186
+ end
187
+
188
+ def disallow_nil_values_in(floats)
189
+ floats.each_pair{|k,v| floats.delete(k) if v.nil? }
190
+ end
191
+
192
+ # cleanup any stale ivars and return the path to result and the resulting bounds
193
+ def resetting_state_afterwards
194
+ begin
195
+ @dest = @dest % [@target_w, @target_h] if File.basename(@dest).include?('%')
196
+ kept = [@dest, @target_w, @target_h]; yield
197
+ ensure
198
+ @source, @dest, @source_w, @dest_w, @source_h, @dest_h = nil
199
+ end
200
+ kept
201
+ end
202
+
203
+ def validate_input_output_files(from_path, to_path)
204
+ @source, @dest = [from_path, to_path].map{|p| File.expand_path(p) }
205
+ destdir = File.dirname(@dest)
206
+ raise MissingInput, "No such file or directory #{@source}" unless File.exist?(@source)
207
+ raise NoDestinationDir, "No destination directory #{destdir}" unless File.exist?(destdir)
208
+ raise DestinationLocked, "Cannot write to #{destdir}" unless File.writable?(destdir)
209
+ raise NoOverwrites, "This will overwrite #{@dest}" if File.exist?(@dest)
210
+ # This will raise if anything happens
211
+ @source_w, @source_h = get_bounds(from_path)
212
+ end
213
+
214
+ def integerize_values_of(h)
215
+ h.each_pair{|k,v| v.nil? ? h.delete(k) : (h[k] = v.to_i)}
216
+ end
217
+
218
+ def wrap_stderr(cmd)
219
+ inp, outp, err = Open3.popen3(cmd)
220
+ error = err.read.to_s.strip
221
+ result = outp.read.strip
222
+ unless self.class::HARMLESS.select{|warning| error =~ warning }.any?
223
+ raise Error, "Problem with #{@source}: #{error}" unless error.nil? || error.empty?
224
+ end
225
+ [inp, outp, err].map{|socket| begin; socket.close; rescue IOError; end }
226
+ result
227
+ end
228
+
229
+ end
230
+
231
+ class ImageProcConvert < ImageProc
232
+ HARMLESS = [/unknown field with tag/]
233
+ def process_exact
234
+ wrap_stderr("convert -resize #{@target_w}x#{@target_h}! #{@source} #{@dest}")
235
+ end
236
+
237
+ def get_bounds(of)
238
+ wrap_stderr("identify #{of}").scan(/(\d+)x(\d+)/)[0].map{|e| e.to_i }
239
+ end
240
+ end
241
+
242
+ class ImageProcRmagick < ImageProc
243
+ def get_bounds(of)
244
+ run_require
245
+ comp = wrap_err { Magick::Image.ping(of)[0] }
246
+ res = comp.columns, comp.rows
247
+ comp = nil; return res
248
+ end
249
+
250
+ def process_exact
251
+ run_require
252
+ img = wrap_err { Magick::Image.read(@source).first }
253
+ img.scale(@target_w, @target_h).write(@dest)
254
+ img = nil # deallocate the ref
255
+ end
256
+ private
257
+ def run_require
258
+ require 'RMagick' unless defined?(Magick)
259
+ end
260
+
261
+ def wrap_err
262
+ begin
263
+ yield
264
+ rescue Magick::ImageMagickError => e
265
+ raise Error, e.to_s
266
+ end
267
+ end
268
+ end
269
+
270
+ class ImageProcSips < ImageProc
271
+ # -Z pixelsWH --resampleHeightWidthMax pixelsWH
272
+ FORMAT_MAP = { ".tif" => "tiff", ".png" => "png", ".tif" => "tiff", ".gif" => "gif" }
273
+ HARMLESS = [/XRefStm encountered but/, /CGColor/]
274
+ def process_exact
275
+ fmt = detect_source_format
276
+ wrap_stderr("sips -s format #{fmt} --resampleHeightWidth #{@target_h} #{@target_w} #{@source} --out '#{@dest}'")
277
+ end
278
+
279
+ def get_bounds(of)
280
+ wrap_stderr("sips #{of} -g pixelWidth -g pixelHeight").scan(/(pixelWidth|pixelHeight): (\d+)/).to_a.map{|e| e[1].to_i}
281
+ end
282
+
283
+ private
284
+ def detect_source_format
285
+ suspected = FORMAT_MAP[File.extname(@source)]
286
+ suspected = (suspected.nil? || suspected.empty?) ? 'jpeg' : suspected
287
+ case suspected
288
+ when "png", "gif"
289
+ raise FormatUnsupported, "SIPS cannot resize indexed color GIF or PNG images, call Apple if you want to know why"
290
+ end
291
+ return suspected
292
+ end
293
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,251 @@
1
+ module ResizeTestHelper
2
+ INPUTS = File.expand_path(File.dirname(__FILE__) + '/input')
3
+ OUTPUTS = File.expand_path(File.dirname(__FILE__) + '/output')
4
+
5
+ def setup
6
+ Dir.glob(File.dirname(__FILE__) + '/output/*.*').map{ |e| FileUtils.rm e }
7
+
8
+ @extensions = ["jpg", "png", "gif"]
9
+ @landscapes = @extensions.map { | ext | "horizontal.#{ext}" }
10
+ @portraits = @extensions.map { | ext | "vertical.#{ext}" }
11
+ @landscape_bounds = 780, 520
12
+ @portraits_bounds = 466, 699
13
+ end
14
+
15
+ def test_get_bounds_raise_when_file_passed_does_not_exist
16
+ assert_raise(ImageProc::Error) do
17
+ @processor.get_bounds("/tmp/__non_existent/#{Time.now.to_i}")
18
+ end
19
+ end
20
+
21
+ def test_get_bounds_raise_when_file_passed_is_not_an_image
22
+ assert_raise(ImageProc::Error) do
23
+ @processor.get_bounds(INPUTS + '/not.an.image.tmp')
24
+ end
25
+ end
26
+
27
+ def test_properly_detects_bounds
28
+ @landscapes.map do |file|
29
+ assert_equal @landscape_bounds, @processor.get_bounds(INPUTS + "/" + file)
30
+ end
31
+
32
+ @portraits.map do |file|
33
+ assert_equal @portraits_bounds, @processor.get_bounds(INPUTS + "/" + file)
34
+ end
35
+ end
36
+
37
+ def test_resize_raises_when_trying_to_overwrite
38
+ assert_raise(ImageProc::NoOverwrites) do
39
+ @processor.resize INPUTS + '/' + @landscapes[0], INPUTS + '/' + @portraits[0], "100x100"
40
+ end
41
+ end
42
+
43
+ def test_resize_raises_when_source_missing
44
+ missing_input = "/tmp/___imageproc_missing.jpg"
45
+ assert !File.exist?(missing_input)
46
+ assert_raise(ImageProc::MissingInput) do
47
+ @processor.resize missing_input, OUTPUTS + '/zeoutput.jpg', "100x100"
48
+ end
49
+ end
50
+
51
+ def test_resize_raises_wheh_destination_dir_missing
52
+ missing_output = "/tmp/___imageproc_missing/__missing.jpg"
53
+ from = (INPUTS + '/' + @landscapes[0])
54
+
55
+ assert !File.exist?(File.dirname(missing_output))
56
+ assert_raise(ImageProc::NoDestinationDir) do
57
+ @processor.resize from, missing_output, "100x100"
58
+ end
59
+ end
60
+
61
+ def test_resize_exact
62
+ names = (@landscapes + @portraits)
63
+ sources = names.map{|file| INPUTS + "/" + file }
64
+
65
+ sources.each_with_index do | source, index |
66
+ assert_nothing_raised do
67
+ path, w, h = @processor.resize_exact(source, OUTPUTS + '/' + names[index], 65, 65)
68
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
69
+ end
70
+
71
+ result_p = OUTPUTS + '/' + File.basename(source)
72
+ assert File.exist?(result_p), "#{result_p} should have been created"
73
+ assert_equal [65, 65], get_bounds(result_p), "The image should have been resized exactly"
74
+ end
75
+ end
76
+
77
+ def test_resize_fitting_proportionally_into_square
78
+ with_each_horizontal_path_and_name do | source, name |
79
+ assert_nothing_raised do
80
+ path, w, h = @processor.resize_fit(source, OUTPUTS + '/' + name, 300, 300)
81
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
82
+ end
83
+
84
+ result_p = OUTPUTS + '/' + File.basename(source)
85
+ assert File.exist?(result_p), "#{result_p} should have been created"
86
+ assert_equal [300, 200], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into rect proortionally"
87
+ end
88
+
89
+ with_each_vertical_path_and_name do | source, name |
90
+ assert_nothing_raised { @processor.resize_fit(source, OUTPUTS + '/' + name, 300, 300) }
91
+
92
+ result_p = OUTPUTS + '/' + File.basename(source)
93
+ assert File.exist?(result_p), "#{result_p} should have been created"
94
+ assert_equal [200, 300 ], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into rect proortionally"
95
+ end
96
+ end
97
+
98
+ def test_fit_square_is_alias_for_proportional_resize
99
+ with_each_horizontal_path_and_name do | source, name |
100
+ assert_nothing_raised do
101
+ path, w, h = @processor.resize_fit_square(source, OUTPUTS + '/' + name, 300)
102
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
103
+ end
104
+
105
+ result_p = OUTPUTS + '/' + File.basename(source)
106
+ assert File.exist?(result_p), "#{result_p} should have been created"
107
+ assert_equal [300, 200], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into rect proortionally"
108
+ end
109
+ end
110
+
111
+ def test_resize_fitting_proportionally_into_portrait
112
+ with_each_horizontal_path_and_name do | source, name |
113
+ assert_nothing_raised do
114
+ path, w, h = @processor.resize_fit(source, OUTPUTS + '/' + name, 20, 100)
115
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
116
+ end
117
+
118
+ result_p = OUTPUTS + '/' + File.basename(source)
119
+ assert File.exist?(result_p), "#{result_p} should have been created"
120
+ assert_equal [20, 13], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into rect proortionally"
121
+ end
122
+ end
123
+
124
+ def test_resize_fitting_proportionally_into_landscape
125
+ with_each_vertical_path_and_name do | source, name |
126
+ assert_nothing_raised do
127
+ path, w, h = @processor.resize_fit(source, OUTPUTS + '/' + name, 100, 20)
128
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
129
+ end
130
+
131
+ result_p = OUTPUTS + '/' + File.basename(source)
132
+ assert File.exist?(result_p), "#{result_p} should have been created"
133
+ assert_equal [13, 20], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into rect proortionally"
134
+ end
135
+ end
136
+
137
+ def test_replaces_wildcards_in_filenames_after_resizing
138
+ source = INPUTS + '/' + @landscapes[0]
139
+ with_wildcards = OUTPUTS + '/resized_%dx%d' + File.extname(@landscapes[0])
140
+ reference_path = with_wildcards % [300, 200]
141
+ assert_nothing_raised do
142
+ path, w, h = @processor.resize_fit(source, with_wildcards, 300, 300)
143
+ assert_equal path, reference_path, "The wildcards should be replaced with computed width and height and the file saved"
144
+ assert_equal [300, 200], get_bounds(reference_path)
145
+ end
146
+ end
147
+
148
+ def test_resize_is_alias_for_fit_with_geometry_string
149
+ with_each_horizontal_path_and_name do | source, name |
150
+ assert_nothing_raised { @processor.resize(source, OUTPUTS + '/' + name, "300x300") }
151
+
152
+ result_p = OUTPUTS + '/' + File.basename(source)
153
+ assert File.exist?(result_p), "#{result_p} should have been created"
154
+ assert_equal [300, 200], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into rect proortionally"
155
+ end
156
+
157
+ with_each_vertical_path_and_name do | source, name |
158
+ assert_nothing_raised do
159
+ path = @processor.resize(source, OUTPUTS + '/' + name, "300x300")
160
+ assert_kind_of String, path, "ImageProc#resize is legacy so it should return the path and nothing else"
161
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result"
162
+ end
163
+
164
+ result_p = OUTPUTS + '/' + File.basename(source)
165
+ assert File.exist?(result_p), "#{result_p} should have been created"
166
+ assert_equal [200, 300 ], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into rect proortionally"
167
+ end
168
+ end
169
+
170
+ def test_resize_fit_width
171
+ with_each_horizontal_path_and_name do | source, name |
172
+ assert_nothing_raised do
173
+ path, w, h = @processor.resize_fit_width(source, OUTPUTS + '/' + name, 400)
174
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
175
+ end
176
+
177
+ result_p = OUTPUTS + '/' + File.basename(source)
178
+ assert File.exist?(result_p), "#{result_p} should have been created"
179
+ assert_equal [400, 267], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been into width"
180
+ end
181
+
182
+ with_each_vertical_path_and_name do | source, name |
183
+ assert_nothing_raised do
184
+ path, w, h = @processor.resize_fit_width(source, OUTPUTS + '/' + name, 400)
185
+ end
186
+
187
+ result_p = OUTPUTS + '/' + File.basename(source)
188
+ assert File.exist?(result_p), "#{result_p} should have been created"
189
+ assert_equal [400, 600 ], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into width"
190
+ end
191
+ end
192
+
193
+ def test_resize_fit_height
194
+ with_each_horizontal_path_and_name do | source, name |
195
+ assert_nothing_raised do
196
+ path, w, h = @processor.resize_fit_height(source, OUTPUTS + '/' + name, 323)
197
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
198
+ end
199
+
200
+ result_p = OUTPUTS + '/' + File.basename(source)
201
+ assert File.exist?(result_p), "#{result_p} should have been created"
202
+ assert_equal [485, 323], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been into width"
203
+ end
204
+
205
+ with_each_vertical_path_and_name do | source, name |
206
+ assert_nothing_raised do
207
+ path, w, h = @processor.resize_fit_width(source, OUTPUTS + '/' + name, 323)
208
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
209
+ end
210
+
211
+ result_p = OUTPUTS + '/' + File.basename(source)
212
+ assert File.exist?(result_p), "#{result_p} should have been created"
213
+ assert_equal [323, 485], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been fit into width"
214
+ end
215
+ end
216
+
217
+ def test_resize_fit_fill
218
+ with_each_horizontal_path_and_name do | source, name |
219
+ assert_nothing_raised do
220
+ path, w, h = @processor.resize_fit_fill(source, OUTPUTS + '/' + name, 260, 250)
221
+ assert_equal OUTPUTS + '/' + File.basename(source), path, "The proc should return the path to the result as first ret"
222
+ end
223
+
224
+ result_p = OUTPUTS + '/' + File.basename(source)
225
+ assert File.exist?(result_p), "#{result_p} should have been created"
226
+ assert_equal [375, 250], get_bounds(result_p), "The image of #{get_bounds(source).join("x")} should have been into width"
227
+ end
228
+ end
229
+
230
+ private
231
+ def get_bounds(of)
232
+ if RUBY_PLATFORM =~ /darwin/i
233
+ `sips #{of} -g pixelWidth -g pixelHeight`.scan(/(pixelWidth|pixelHeight): (\d+)/).to_a.map{|e| e[1]}
234
+ else
235
+ `identify #{of}`.scan(/(\d+)x(\d+)/)[0]
236
+ end.map{|e| e.to_i}
237
+ end
238
+
239
+ def with_each_vertical_path_and_name
240
+ @portraits.each do | file |
241
+ yield(INPUTS + "/" + file, file)
242
+ end
243
+ end
244
+
245
+ def with_each_horizontal_path_and_name
246
+ @landscapes.each do | file |
247
+ yield(INPUTS + "/" + file, file)
248
+ end
249
+ end
250
+
251
+ end
@@ -0,0 +1,112 @@
1
+ require 'test/unit'
2
+ require 'fileutils'
3
+ require 'resize_test_helper'
4
+ require 'image_proc'
5
+
6
+ class TestGeometryFitting < Test::Unit::TestCase
7
+ def setup
8
+ @x = ImageProc.new
9
+ class << @x
10
+ public :fit_sizes
11
+ end
12
+ end
13
+
14
+ def test_fit_width
15
+ bounds = [1024, 500]
16
+ assert_equal [50, 24], @x.fit_sizes(bounds, :width => 50)
17
+ assert_equal [50, 24], @x.fit_sizes(bounds, :width => 50, :height => nil)
18
+
19
+ bounds = [500, 1024]
20
+ assert_equal [50, 102], @x.fit_sizes(bounds, :width => 50)
21
+ end
22
+
23
+ def test_fit_width_should_not_modify_values_if_image_fits_already
24
+ bounds = [100, 400]
25
+ assert_equal bounds, @x.fit_sizes(bounds, :width => 100)
26
+ end
27
+
28
+ def test_fit_height_should_not_modify_values_if_image_fits_already
29
+ bounds = [100, 400]
30
+ assert_equal bounds, @x.fit_sizes(bounds, :height => 400)
31
+ end
32
+
33
+ def test_fit_height
34
+ bounds = [1024, 500]
35
+ assert_equal [102, 50], @x.fit_sizes(bounds, :height => 50)
36
+ assert_equal [102, 50], @x.fit_sizes(bounds, :height => 50, :width => nil)
37
+
38
+ bounds = [500, 1024]
39
+ assert_equal [24, 50], @x.fit_sizes(bounds, :height => 50)
40
+ end
41
+
42
+ def test_fit_both
43
+ bounds = 1024, 500
44
+ assert_equal [70, 34], @x.fit_sizes(bounds, :height => 70, :width => 70)
45
+
46
+ bounds = 500, 1024
47
+ assert_equal [34, 70], @x.fit_sizes(bounds, :height => 70, :width => 70)
48
+
49
+ bounds = 780, 520
50
+ assert_equal [120, 80], @x.fit_sizes(bounds, :height => 120, :width => 120)
51
+ end
52
+
53
+ def test_fit_groks_strings_too
54
+ bounds = [1024, 500]
55
+ assert_equal [70, 34], @x.fit_sizes(bounds, :height => "70", :width => "70")
56
+ end
57
+
58
+ def test_fit_rejects_nil_values
59
+ bounds = [1024, 500]
60
+ assert_equal [50, 24], @x.fit_sizes(bounds, :width => 50, :height => nil)
61
+ assert_equal [102, 50], @x.fit_sizes(bounds, :height => 50, :width => nil)
62
+ end
63
+
64
+ def test_fit_nevere_produces_zeroes
65
+ bounds = [1000, 20]
66
+ assert_equal [20, 1], @x.fit_sizes(bounds, :width => 20, :height => 20)
67
+ end
68
+ end
69
+
70
+ class TestGeometryFittingWithCrop < Test::Unit::TestCase
71
+ def setup
72
+ @x = ImageProc.new
73
+ end
74
+
75
+ def test_fit_suare_into_smaller_square_requires_no_cropping
76
+ bounds = [100, 100]
77
+ assert_equal [20, 20], @x.fit_sizes_with_crop(bounds, :width => 20, :height => 20)
78
+ end
79
+
80
+ def test_fit_landscape_into_square_will_slice_off_left_and_right
81
+ bounds = [768, 575]
82
+ assert_equal [27, 20], @x.fit_sizes_with_crop(bounds, :width => 20, :height => 20)
83
+
84
+ bounds = [200, 40]
85
+ assert_equal [100, 20], @x.fit_sizes_with_crop(bounds, :width => 20, :height => 20)
86
+ end
87
+
88
+ def test_fit_portrait_into_square_will_slice_off_top_and_bottom
89
+ bounds = [576, 768]
90
+ assert_equal [300, 400], @x.fit_sizes_with_crop(bounds, :width => 300, :height => 300)
91
+ end
92
+
93
+ def test_fit_portrait_into_portrait_will_resize
94
+ bounds = [576, 768]
95
+ assert_equal [200, 267], @x.fit_sizes_with_crop(bounds, :width => 200, :height => 210)
96
+ end
97
+
98
+ def test_fit_landscape_into_portrait_will_resize
99
+ bounds = [768, 576]
100
+ assert_equal [280, 210], @x.fit_sizes_with_crop(bounds, :width => 200, :height => 210)
101
+ end
102
+
103
+ def test_fit_with_crop_does_not_lie_when_small_floats_might_be_involved
104
+ bounds = [20, 1000]
105
+ assert_equal bounds, @x.fit_sizes_with_crop(bounds, :width => 20, :height => 20)
106
+ end
107
+
108
+ def test_fit_with_crop_should_not_overnudge
109
+ bounds = [780, 520]
110
+ assert_equal [375, 250], @x.fit_sizes_with_crop(bounds, :width => 260, :height => 250)
111
+ end
112
+ end
@@ -0,0 +1,102 @@
1
+ require 'test/unit'
2
+ require 'fileutils'
3
+ require 'resize_test_helper'
4
+ require 'image_proc'
5
+
6
+ #class TestQuickProcessViaClassWithGeomString < Test::Unit::TestCase
7
+ # def test_works
8
+ # source = File.dirname(__FILE__) + '/input/horizontal.jpg'
9
+ # dest = File.dirname(__FILE__) + '/output/resized.jpg'
10
+ # assert_nothing_raised { ImageProc.resize(source, dest, "50x50") }
11
+ # FileUtils.rm dest
12
+ # end
13
+ #end
14
+
15
+ class TestQuickProcessWithOptions < Test::Unit::TestCase
16
+ def test_resize_with_options
17
+ source = File.dirname(__FILE__) + '/input/horizontal.jpg'
18
+ dest = File.dirname(__FILE__) + '/output/resized.jpg'
19
+ opts = {:height=>75, :fill=>true}
20
+ begin
21
+ assert_nothing_raised do
22
+ path, w, h = ImageProc.resize_with_options(source, dest, opts)
23
+ end
24
+ ensure
25
+ File.unlink(dest) rescue nil
26
+ end
27
+ end
28
+
29
+ def test_raises_on_invalid_options
30
+ assert_raise(ImageProc::InvalidOptions) do
31
+ source = File.dirname(__FILE__) + '/input/horizontal.jpg'
32
+ dest = File.dirname(__FILE__) + '/output/resized.jpg'
33
+ opts = {:too => 4, :doo => 10}
34
+ ImageProc.resize_with_options(source, dest, opts)
35
+ end
36
+
37
+ assert_raise(ImageProc::InvalidOptions) do
38
+ source = File.dirname(__FILE__) + '/input/horizontal.jpg'
39
+ dest = File.dirname(__FILE__) + '/output/resized.jpg'
40
+ opts = {}
41
+ ImageProc.resize_with_options(source, dest, opts)
42
+ end
43
+ end
44
+ end
45
+
46
+ class TestEngineAssignmentSticks < Test::Unit::TestCase
47
+ def test_foreign_engine_assignment_sticks
48
+ dummy = "foobar"
49
+ ImageProc.engine = dummy
50
+ assert_equal dummy, ImageProc.engine
51
+ ImageProc.engine = nil
52
+ end
53
+ end
54
+
55
+ if RUBY_PLATFORM =~ /darwin/i
56
+ class TestImageProcSips < Test::Unit::TestCase
57
+ def setup
58
+ super
59
+ @processor = ImageProcSips.new
60
+ @landscapes.reject!{|e| e =~ /\.(png|gif)/}
61
+ @portraits.reject!{|e| e =~ /\.(png|gif)/}
62
+ end
63
+
64
+ def test_sips_does_not_grok_pngs
65
+ assert_raise(ImageProc::FormatUnsupported) do
66
+ @processor.resize(INPUTS + '/horizontal.gif', OUTPUTS + '/horizontal.gif', "100x100")
67
+ end
68
+ end
69
+
70
+ def test_sips_does_not_grok_gifs
71
+ assert_raise(ImageProc::FormatUnsupported) do
72
+ @processor.resize(INPUTS + '/horizontal.png', OUTPUTS + '/horizontal.png', "100x100")
73
+ end
74
+ end
75
+
76
+ include ResizeTestHelper
77
+ end
78
+ end
79
+
80
+ if(`which convert`)
81
+ class TestImageProcConvert < Test::Unit::TestCase
82
+ def setup
83
+ super
84
+ @processor = ImageProcConvert.new
85
+ end
86
+
87
+ include ResizeTestHelper
88
+ end
89
+ end
90
+
91
+ begin
92
+ require 'RMagick'
93
+ class TestImageProcRmagick < Test::Unit::TestCase
94
+ def setup
95
+ super
96
+ @processor = ImageProcRmagick.new
97
+ end
98
+
99
+ include ResizeTestHelper
100
+ end
101
+ rescue LoadError
102
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: image_proc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Julik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-09 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.2
24
+ version:
25
+ description: A no-frills image metadata and resizing
26
+ email:
27
+ - me@julik.nl
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - init.rb
42
+ - lib/image_proc.rb
43
+ - test/input/horizontal.gif
44
+ - test/input/horizontal.jpg
45
+ - test/input/horizontal.png
46
+ - test/input/vertical.gif
47
+ - test/input/vertical.jpg
48
+ - test/input/vertical.png
49
+ - test/resize_test_helper.rb
50
+ - test/test_geom.rb
51
+ - test/test_image_proc.rb
52
+ has_rdoc: true
53
+ homepage: http://wiretap.rubyforge.org/image_proc
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --main
57
+ - README.txt
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project: wiretap
75
+ rubygems_version: 1.3.1
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: A no-frills image resizer, with pluggable backends
79
+ test_files:
80
+ - test/test_geom.rb
81
+ - test/test_image_proc.rb