image_proc 0.1.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.
@@ -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