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