image_proc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/Manifest.txt +15 -0
- data/README.txt +36 -0
- data/Rakefile +10 -0
- data/init.rb +1 -0
- data/lib/image_proc.rb +293 -0
- data/test/input/horizontal.gif +0 -0
- data/test/input/horizontal.jpg +0 -0
- data/test/input/horizontal.png +0 -0
- data/test/input/vertical.gif +0 -0
- data/test/input/vertical.jpg +0 -0
- data/test/input/vertical.png +0 -0
- data/test/resize_test_helper.rb +251 -0
- data/test/test_geom.rb +112 -0
- data/test/test_image_proc.rb +102 -0
- metadata +81 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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'
|
data/lib/image_proc.rb
ADDED
@@ -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
|
data/test/test_geom.rb
ADDED
@@ -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
|