insano_image_resizer 0.5.7 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +21 -8
- data/lib/insano_image_resizer/processor.rb +73 -43
- metadata +17 -17
data/README.md
CHANGED
@@ -7,7 +7,7 @@ in the frame when possible. The resizer is built on top of the VIPS command-line
|
|
7
7
|
and is designed to be as fast as possible. In our tests, VIPS is faster than ImageMagick for any
|
8
8
|
image larger than ~300x300px, and is exponentially faster for very large images.
|
9
9
|
|
10
|
-
![A brief overview of the Insano processing function](
|
10
|
+
![A brief overview of the Insano processing function](samples/explanation.png)
|
11
11
|
|
12
12
|
Output formats: The Insano image resizer will keep PNGs as PNG, but any other format is converted to JPEG
|
13
13
|
|
@@ -36,7 +36,7 @@ Example:
|
|
36
36
|
|
37
37
|
Input parameters:
|
38
38
|
|
39
|
-
The
|
39
|
+
The #process method is the main function of the Insano gem. Using different parameters,
|
40
40
|
you can produce a wide range of resized images. Each of the parameters is explained below.
|
41
41
|
|
42
42
|
The first argument is an input file path. Because the Insano image resizer uses the VIPS
|
@@ -50,6 +50,13 @@ the current width and height of the image. Note that the image resizer will
|
|
50
50
|
never distort an image: the output image will always fill the viewport you provide,
|
51
51
|
scaling up only if absolutely necessary.
|
52
52
|
|
53
|
+
Note that the output image may show more of the source image than you specify using the
|
54
|
+
viewport. The region is only meant to indicate what region you'd like to ensure makes
|
55
|
+
it into the output. For example, if you have a 200 x 200px image and request an output image of
|
56
|
+
100 x 100px, showing the 50px region around 150px x 150px, the output will contain more than
|
57
|
+
just that region, since filling a 100px square with a 50px region would require enlarging the
|
58
|
+
source image.
|
59
|
+
|
53
60
|
The third parameter is the point of interest that you'd like to keep centered if possible.
|
54
61
|
Imagine that an 4:3 image contains a person's face on the left side. When you create a
|
55
62
|
square thumbnail of the image, the persons face is half chopped off, because the processor
|
@@ -61,12 +68,18 @@ should be cropped off. However, specifying the optional :region parameter with a
|
|
61
68
|
less than 1, you can make the image resizer zoom in around the POI, cropping the image
|
62
69
|
so that an area of size (region * image size) around the POI is visible.
|
63
70
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
71
|
+
The last optional parameter determines quality. The #process method accepts either a hash or an
|
72
|
+
integer. If an integer is provided, then that number will be used when generating all JPEG files.
|
73
|
+
A hash of the following form may be used instead:
|
74
|
+
|
75
|
+
{ :min_area => { :area => 4000, :quality => 90 },
|
76
|
+
:max_area => { :area => 1000000, :quality => 60 }}
|
77
|
+
|
78
|
+
When a hash is used, images up to the minimum area use the min_area quality. Images at or above the
|
79
|
+
maximum area use the max_area quality. Images in between get a quality based on a sliding scale
|
80
|
+
between the min and max. This is the default behavior.
|
81
|
+
|
82
|
+
Insano accounts for EXIF rotation flags, producing properly oriented images across all devices.
|
70
83
|
|
71
84
|
Credits
|
72
85
|
=======
|
@@ -9,28 +9,54 @@ module InsanoImageResizer
|
|
9
9
|
include Loggable
|
10
10
|
include Cocaine
|
11
11
|
|
12
|
+
DEFAULT_QUALITY_LIMITS = { :min_area => { :area => 4000, :quality => 90 },
|
13
|
+
:max_area => { :area => 1000000, :quality => 60 }}
|
14
|
+
|
12
15
|
def initialize(options = {})
|
13
16
|
@vips_path = options[:vips_path] || 'vips'
|
14
17
|
@identify_path = options[:identify_path] || 'identify'
|
15
18
|
end
|
16
19
|
|
17
|
-
def process(input_path, viewport_size = {}, interest_point = {},
|
20
|
+
def process(input_path, viewport_size = {}, interest_point = {}, quality_limits=DEFAULT_QUALITY_LIMITS)
|
18
21
|
width, height, original_format, target_extension = fetch_image_properties(input_path)
|
19
22
|
|
23
|
+
exif_result = handle_exif_rotation(input_path, original_format)
|
24
|
+
width, height = [height, width] if exif_result == :swap_dimensions
|
25
|
+
|
20
26
|
output_tmp = Tempfile.new(['img', ".#{target_extension}"])
|
21
27
|
|
22
28
|
transform = calculate_transform(input_path, width, height, viewport_size, interest_point)
|
29
|
+
quality = target_jpg_quality(transform[:w], transform[:h], quality_limits) if target_extension == 'jpg'
|
23
30
|
run_transform(input_path, output_tmp.path, transform, original_format, target_extension, quality)
|
24
31
|
|
25
32
|
output_tmp.path
|
26
33
|
end
|
27
34
|
|
35
|
+
# limits is of the form:
|
36
|
+
# { :min_area => { :area => 4000, :quality => 90 },
|
37
|
+
# :max_area => { :area => 1000000, :quality => 60 }}
|
38
|
+
def target_jpg_quality(width, height, limits)
|
39
|
+
return limits.to_i unless limits.is_a?(Hash)
|
40
|
+
min_area = limits[:min_area][:area]
|
41
|
+
min_area_quality = limits[:min_area][:quality]
|
42
|
+
max_area = limits[:max_area][:area]
|
43
|
+
max_area_quality = limits[:max_area][:quality]
|
44
|
+
normalized_target_area = [width * height - min_area, 0].max
|
45
|
+
normalized_max_area = max_area - min_area
|
46
|
+
target_area_fraction = [normalized_target_area.to_f / normalized_max_area, 1].min
|
47
|
+
quality_span = min_area_quality - max_area_quality
|
48
|
+
quality_fraction = quality_span * target_area_fraction
|
49
|
+
min_area_quality - quality_fraction
|
50
|
+
end
|
51
|
+
|
28
52
|
private
|
29
53
|
|
30
54
|
def fetch_image_properties(input_path)
|
31
55
|
line = Cocaine::CommandLine.new(@identify_path, '-format "%w %h %m" :input')
|
32
|
-
|
33
|
-
|
56
|
+
width, height, original_format = line.run(:input => input_path).split(' ')
|
57
|
+
|
58
|
+
target_extension = (original_format == 'PNG' ? 'png' : 'jpg')
|
59
|
+
[width.to_i, height.to_i, original_format, target_extension]
|
34
60
|
end
|
35
61
|
|
36
62
|
def calculate_transform(input_path, width, height, viewport_size, interest_point)
|
@@ -157,8 +183,7 @@ module InsanoImageResizer
|
|
157
183
|
# but don't seem to be used.
|
158
184
|
# The last four params define a rect of the source image that is transformed.
|
159
185
|
|
160
|
-
quality_extension = ''
|
161
|
-
quality_extension = ":#{quality}" if output_extension == 'jpg'
|
186
|
+
quality_extension = quality ? ":#{quality}" : ''
|
162
187
|
|
163
188
|
if (transform[:scale] < 0.5)
|
164
189
|
# If we're shrinking the image by more than a factor of two, let's do a two-pass operation. The reason we do this
|
@@ -169,21 +194,18 @@ module InsanoImageResizer
|
|
169
194
|
|
170
195
|
# To ensure that we actually do both passes, don't let the im_shrink go all the way. This will result in terrible
|
171
196
|
# looking shrunken images, since im_shrink basically just cuts pixels out.
|
172
|
-
if
|
173
|
-
shrink_factor -= 1
|
174
|
-
end
|
197
|
+
shrink_factor -= 1 if shrink_factor == 1.0 / transform[:scale]
|
175
198
|
|
176
199
|
transform[:scale] *= shrink_factor
|
177
200
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
intermediate_path = input_path[0..-4]+"_shrunk." + output_extension
|
201
|
+
|
202
|
+
intermediate_path = "#{input_path}_shrunk.#{output_extension}"
|
203
|
+
intermediate_quality_extension = quality ? ":90" : ''
|
204
|
+
|
183
205
|
|
184
206
|
line = Cocaine::CommandLine.new(@vips_path, "im_shrink :input :intermediate_path :shrink_factor :shrink_factor")
|
185
207
|
line.run(:input => input_path,
|
186
|
-
:intermediate_path => "#{intermediate_path}#{
|
208
|
+
:intermediate_path => "#{intermediate_path}#{intermediate_quality_extension}",
|
187
209
|
:shrink_factor => shrink_factor.to_s)
|
188
210
|
|
189
211
|
|
@@ -209,40 +231,48 @@ module InsanoImageResizer
|
|
209
231
|
:h => transform[:h].to_s)
|
210
232
|
end
|
211
233
|
|
212
|
-
|
213
|
-
orientation = 0
|
214
|
-
orientation = EXIFR::JPEG.new(input_path).orientation.to_i if original_format == 'JPEG'
|
215
|
-
|
216
|
-
if orientation == 3 || orientation == 6 || orientation == 8
|
217
|
-
FileUtils.mv(output_path, intermediate_path)
|
218
|
-
o_transform = []
|
219
|
-
if orientation == 3
|
220
|
-
command = 'im_rot180'
|
221
|
-
elsif orientation == 6
|
222
|
-
command = 'im_rot90'
|
223
|
-
elsif orientation == 8
|
224
|
-
command = 'im_rot270'
|
225
|
-
else
|
226
|
-
command = nil
|
227
|
-
end
|
228
|
-
|
229
|
-
if command
|
230
|
-
line = Cocaine::CommandLine.new(@vips_path, ":command :intermediate_path :output")
|
231
|
-
line.run(:output => "#{output_path}#{quality_extension}",
|
232
|
-
:intermediate_path => intermediate_path,
|
233
|
-
:command => command)
|
234
|
-
end
|
234
|
+
FileUtils.rm(input_path)
|
235
235
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
236
|
+
|
237
|
+
output_path
|
238
|
+
end
|
239
|
+
|
240
|
+
def handle_exif_rotation(input_path, original_format)
|
241
|
+
return unless original_format == 'JPEG'
|
242
|
+
|
243
|
+
# find the EXIF values
|
244
|
+
orientation = EXIFR::JPEG.new(input_path).orientation.to_i
|
245
|
+
|
246
|
+
if orientation == 3
|
247
|
+
command = 'im_rot180'
|
248
|
+
elsif orientation == 6
|
249
|
+
command = 'im_rot90'
|
250
|
+
return_value = :swap_dimensions
|
251
|
+
elsif orientation == 8
|
252
|
+
command = 'im_rot270'
|
253
|
+
return_value = :swap_dimensions
|
254
|
+
else
|
255
|
+
return
|
240
256
|
end
|
241
257
|
|
242
|
-
|
258
|
+
intermediate_path = "#{input_path}_rotated.jpg"
|
259
|
+
intermediate_quality_extension = ':90'
|
243
260
|
|
244
|
-
|
261
|
+
line = Cocaine::CommandLine.new(@vips_path, ":command :input :intermediate_path")
|
262
|
+
line.run(:input => input_path,
|
263
|
+
:intermediate_path => "#{intermediate_path}#{intermediate_quality_extension}",
|
264
|
+
:command => command)
|
265
|
+
|
266
|
+
# mogrify strips the EXIF tags so that browsers that support EXIF don't rotate again after
|
267
|
+
# we have rotated
|
268
|
+
line = Cocaine::CommandLine.new('mogrify', "-strip :intermediate_path")
|
269
|
+
line.run(:intermediate_path => intermediate_path)
|
270
|
+
|
271
|
+
FileUtils.mv(intermediate_path, input_path)
|
272
|
+
|
273
|
+
return_value
|
245
274
|
end
|
275
|
+
|
246
276
|
end
|
247
277
|
end
|
248
278
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: insano_image_resizer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-03-
|
12
|
+
date: 2013-03-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: cocaine
|
16
|
-
requirement: &
|
16
|
+
requirement: &2164743100 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *2164743100
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &2164742380 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *2164742380
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: jeweler
|
38
|
-
requirement: &
|
38
|
+
requirement: &2164741660 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *2164741660
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: pry
|
49
|
-
requirement: &
|
49
|
+
requirement: &2164740980 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *2164740980
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: pry-nav
|
60
|
-
requirement: &
|
60
|
+
requirement: &2164740340 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *2164740340
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pry-stack_explorer
|
71
|
-
requirement: &
|
71
|
+
requirement: &2164739640 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *2164739640
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: exifr
|
82
|
-
requirement: &
|
82
|
+
requirement: &2164737920 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,7 +87,7 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *2164737920
|
91
91
|
description: ! "An image resizing gem that generates smaller versions of requested
|
92
92
|
\n images. Calls through to VIPS on the command line to perform
|
93
93
|
processing,\n and automatically handles cropping and scaling
|
@@ -121,7 +121,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
121
|
version: '0'
|
122
122
|
segments:
|
123
123
|
- 0
|
124
|
-
hash:
|
124
|
+
hash: 1771295257992981064
|
125
125
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
126
|
none: false
|
127
127
|
requirements:
|