insano_image_resizer 0.3.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/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +45 -0
- data/LICENSE +23 -0
- data/README.md +77 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/image_resizer/configurable.rb +206 -0
- data/lib/image_resizer/loggable.rb +28 -0
- data/lib/image_resizer/processor.rb +199 -0
- data/lib/image_resizer/shell.rb +48 -0
- data/lib/image_resizer.rb +5 -0
- data/samples/explanation.png +0 -0
- data/samples/test.jpg +0 -0
- data/samples/test.png +0 -0
- data/spec/image_resizer/configurable_spec.rb +479 -0
- data/spec/image_resizer/loggable_spec.rb +80 -0
- data/spec/image_resizer/processor_spec.rb +144 -0
- data/spec/image_resizer/shell_spec.rb +34 -0
- data/spec/spec_helper.rb +61 -0
- metadata +152 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@insano_image_resizer
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
binding_of_caller (0.6.7)
|
5
|
+
coderay (1.0.6)
|
6
|
+
diff-lcs (1.1.3)
|
7
|
+
git (1.2.5)
|
8
|
+
jeweler (1.8.3)
|
9
|
+
bundler (~> 1.0)
|
10
|
+
git (>= 1.2.5)
|
11
|
+
rake
|
12
|
+
rdoc
|
13
|
+
json (1.6.6)
|
14
|
+
method_source (0.7.1)
|
15
|
+
pry (0.9.8.4)
|
16
|
+
coderay (~> 1.0.5)
|
17
|
+
method_source (~> 0.7.1)
|
18
|
+
slop (>= 2.4.4, < 3)
|
19
|
+
pry-nav (0.2.0)
|
20
|
+
pry (~> 0.9.8.1)
|
21
|
+
pry-stack_explorer (0.4.1)
|
22
|
+
binding_of_caller (~> 0.6.2)
|
23
|
+
pry (~> 0.9.8.2)
|
24
|
+
rake (0.9.2.2)
|
25
|
+
rdoc (3.12)
|
26
|
+
json (~> 1.4)
|
27
|
+
rspec (2.9.0)
|
28
|
+
rspec-core (~> 2.9.0)
|
29
|
+
rspec-expectations (~> 2.9.0)
|
30
|
+
rspec-mocks (~> 2.9.0)
|
31
|
+
rspec-core (2.9.0)
|
32
|
+
rspec-expectations (2.9.1)
|
33
|
+
diff-lcs (~> 1.1.3)
|
34
|
+
rspec-mocks (2.9.0)
|
35
|
+
slop (2.4.4)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
ruby
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
jeweler
|
42
|
+
pry
|
43
|
+
pry-nav
|
44
|
+
pry-stack_explorer
|
45
|
+
rspec
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
Copyright (c) 2012 J. Benjamin Gotow
|
3
|
+
Development sponsored by Populr, Inc
|
4
|
+
http://www.populr.me
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
Insano Image Resizer
|
2
|
+
====================
|
3
|
+
|
4
|
+
The Insano image resizer allows you to create resized versions of images, specifying a
|
5
|
+
desired width and height as well as a point of interest that the resizer will keep centered
|
6
|
+
in the frame when possible. The resizer is built on top of the VIPS command-line tool,
|
7
|
+
and is designed to be as fast as possible. In our tests, VIPS is faster than ImageMagick for any
|
8
|
+
image larger than ~300x300px, and is exponentially faster for very large images.
|
9
|
+
|
10
|
+

|
11
|
+
|
12
|
+
Output formats: The Insano image resizer will produce either a PNG or JPG image, depending
|
13
|
+
on whether the source image includes transparency.
|
14
|
+
|
15
|
+
* Insano is the fastest waterslide in the world. This isn't a waterslide, but it's similarly fast.
|
16
|
+
|
17
|
+
Usage
|
18
|
+
=====
|
19
|
+
|
20
|
+
In your Gemfile:
|
21
|
+
|
22
|
+
gem 'insano_resizer'
|
23
|
+
|
24
|
+
Example:
|
25
|
+
|
26
|
+
# Specify an existing image in any format supported by VIPS
|
27
|
+
input_path = 'samples/test.jpg'
|
28
|
+
|
29
|
+
# Create a new instance of the Image processor
|
30
|
+
processor = ImageResizer::Processor.new
|
31
|
+
|
32
|
+
# Process the image, creating a temporary file.
|
33
|
+
output_path = processor.process(input_path, {w: 100, h: 200}, {x:986, y:820, region: 0.5})
|
34
|
+
|
35
|
+
# Move the image to an output path
|
36
|
+
FileUtils.mv(output_path, 'samples/output/test.jpg')
|
37
|
+
|
38
|
+
Input parameters:
|
39
|
+
|
40
|
+
The `process` method is the main function of the Insano gem. Using different parameters,
|
41
|
+
you can produce a wide range of resized images. Each of the parameters is explained below.
|
42
|
+
|
43
|
+
The first argument is an input file path. Because the Insano image resizer uses the VIPS
|
44
|
+
command line, it is not possible to transform an image that has been loaded into memory.
|
45
|
+
|
46
|
+
The second argument is a viewport hash containing width and height keys.
|
47
|
+
You can specify both width and height to produce an output image of a specific size, or provide
|
48
|
+
only width or height to have the resizer compute the other dimension based
|
49
|
+
on the aspect ratio of the image. Finally, you can pass an empty hash to use
|
50
|
+
the current width and height of the image. Note that the image resizer will
|
51
|
+
never distort an image: the output image will always fill the viewport you provide,
|
52
|
+
scaling up only if absolutely necessary.
|
53
|
+
|
54
|
+
The third parameter is the point of interest that you'd like to keep centered if possible.
|
55
|
+
Imagine that an 4:3 image contains a person's face on the left side. When you create a
|
56
|
+
square thumbnail of the image, the persons face is half chopped off, because the processor
|
57
|
+
trims off the left and right uniformly. Specifying a point of interest allows you to correct
|
58
|
+
for this problem.
|
59
|
+
|
60
|
+
By default, the POI is used only when cropping the image and deciding which sides
|
61
|
+
should be cropped off. However, specifying the optional :region parameter with a value
|
62
|
+
less than 1, you can make the image resizer zoom in around the POI, cropping the image
|
63
|
+
so that an area of size (region * image size) around the POI is visible.
|
64
|
+
|
65
|
+
Note that the output image may show more of the source image than you specify using the
|
66
|
+
interest region. The region is only meant to indicate what region you'd like to ensure makes
|
67
|
+
it into the output. For example, if you have a 200 x 200px image and request an output image of
|
68
|
+
100 x 100px, showing the 50px region around 150px x 150px, the output will contain more than
|
69
|
+
just that region, since filling a 100px square with a 50px region would require enlarging the
|
70
|
+
source image.
|
71
|
+
|
72
|
+
Credits
|
73
|
+
=======
|
74
|
+
|
75
|
+
This project is loosely based off of the ImageResizer gem previously authored by Daniel Nelson of Populr.me.
|
76
|
+
It draws heavily on the VIPS im_affinei command line function to resize images using an affine transform.
|
77
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "insano_image_resizer"
|
18
|
+
gem.homepage = "http://github.com/populr/insano_image_resizer"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{An image resizing gem that calls through to VIPS on the command line.}
|
21
|
+
gem.description = %Q{An image resizing gem that generates smaller versions of requested
|
22
|
+
images. Calls through to VIPS on the command line to perform processing,
|
23
|
+
and automatically handles cropping and scaling the requested image, taking
|
24
|
+
a point of interest into account if requested.}
|
25
|
+
gem.email = "ben@populr.me"
|
26
|
+
gem.authors = ["Ben Gotow"]
|
27
|
+
# dependencies defined in Gemfile
|
28
|
+
gem.files.exclude 'spec'
|
29
|
+
gem.files.exclude 'samples'
|
30
|
+
gem.files.exclude 'tmp'
|
31
|
+
gem.files.exclude 'demo.rb'
|
32
|
+
end
|
33
|
+
Jeweler::RubygemsDotOrgTasks.new
|
34
|
+
|
35
|
+
require 'rspec/core'
|
36
|
+
require 'rspec/core/rake_task'
|
37
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
38
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
39
|
+
end
|
40
|
+
|
41
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
42
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
43
|
+
spec.rcov = true
|
44
|
+
end
|
45
|
+
|
46
|
+
task :default => :spec
|
47
|
+
|
48
|
+
require 'rdoc/task'
|
49
|
+
Rake::RDocTask.new do |rdoc|
|
50
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
51
|
+
|
52
|
+
rdoc.rdoc_dir = 'rdoc'
|
53
|
+
rdoc.title = "image_resizer #{version}"
|
54
|
+
rdoc.rdoc_files.include('README*')
|
55
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module ImageResizer
|
2
|
+
module Configurable
|
3
|
+
|
4
|
+
# Exceptions
|
5
|
+
class NotConfigured < RuntimeError; end
|
6
|
+
class BadConfigAttribute < RuntimeError; end
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
klass.class_eval do
|
10
|
+
include Configurable::InstanceMethods
|
11
|
+
extend Configurable::ClassMethods
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class DeferredBlock # Inheriting from Proc causes errors in some versions of Ruby
|
16
|
+
def initialize(blk)
|
17
|
+
@blk = blk
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
@blk.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module InstanceMethods
|
26
|
+
|
27
|
+
def configure(&block)
|
28
|
+
yield ConfigurationProxy.new(self)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def configure_with(config, *args, &block)
|
33
|
+
config = saved_config_for(config) if config.is_a?(Symbol)
|
34
|
+
config.apply_configuration(self, *args)
|
35
|
+
configure(&block) if block
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_config_method?(method_name)
|
40
|
+
config_methods.include?(method_name.to_sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
def configuration
|
44
|
+
@configuration ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def config_methods
|
48
|
+
@config_methods ||= self.class.config_methods.dup
|
49
|
+
end
|
50
|
+
|
51
|
+
def default_configuration
|
52
|
+
@default_configuration ||= self.class.default_configuration.dup
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_config_value(key, value)
|
56
|
+
configuration[key] = value
|
57
|
+
child_configurables.each{|c| c.set_if_unset(key, value) }
|
58
|
+
value
|
59
|
+
end
|
60
|
+
|
61
|
+
def use_as_fallback_config(other_configurable)
|
62
|
+
other_configurable.add_child_configurable(self)
|
63
|
+
self.fallback_configurable = other_configurable
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def add_child_configurable(obj)
|
69
|
+
child_configurables << obj
|
70
|
+
config_methods.push(*obj.config_methods)
|
71
|
+
fallback_configurable.config_methods.push(*obj.config_methods) if fallback_configurable
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_if_unset(key, value)
|
75
|
+
set_config_value(key, value) unless set_locally?(key)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
attr_accessor :fallback_configurable
|
81
|
+
|
82
|
+
def child_configurables
|
83
|
+
@child_configurables ||= []
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_locally?(key)
|
87
|
+
instance_variable_defined?("@#{key}")
|
88
|
+
end
|
89
|
+
|
90
|
+
def default_value(key)
|
91
|
+
if default_configuration[key].is_a?(DeferredBlock)
|
92
|
+
default_configuration[key] = default_configuration[key].call
|
93
|
+
end
|
94
|
+
default_configuration[key]
|
95
|
+
end
|
96
|
+
|
97
|
+
def saved_configs
|
98
|
+
self.class.saved_configs
|
99
|
+
end
|
100
|
+
|
101
|
+
def saved_config_for(symbol)
|
102
|
+
config = saved_configs[symbol]
|
103
|
+
if config.nil?
|
104
|
+
raise ArgumentError, "#{symbol.inspect} is not a known configuration - try one of #{saved_configs.keys.join(', ')}"
|
105
|
+
end
|
106
|
+
config = config.call if config.respond_to?(:call)
|
107
|
+
config
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
module ClassMethods
|
113
|
+
|
114
|
+
def default_configuration
|
115
|
+
@default_configuration ||= configurable_ancestors.reverse.inject({}) do |default_config, klass|
|
116
|
+
default_config.merge!(klass.default_configuration)
|
117
|
+
default_config
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def config_methods
|
122
|
+
@config_methods ||= configurable_ancestors.inject([]) do |conf_methods, klass|
|
123
|
+
conf_methods |= klass.config_methods
|
124
|
+
conf_methods
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def nested_configurables
|
129
|
+
@nested_configurables ||= []
|
130
|
+
end
|
131
|
+
|
132
|
+
def register_configuration(name, config=nil, &config_in_block)
|
133
|
+
saved_configs[name] = config_in_block || config
|
134
|
+
end
|
135
|
+
|
136
|
+
def saved_configs
|
137
|
+
@saved_configs ||= {}
|
138
|
+
end
|
139
|
+
|
140
|
+
def configurable_ancestors
|
141
|
+
@configurable_ancestors ||= ancestors.select{|a| a.included_modules.include?(Configurable) } - [self]
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def configurable_attr attribute, default=nil, &blk
|
147
|
+
default_configuration[attribute] = blk ? DeferredBlock.new(blk) : default
|
148
|
+
|
149
|
+
# Define the reader
|
150
|
+
define_method(attribute) do
|
151
|
+
configuration.has_key?(attribute) ? configuration[attribute] : default_value(attribute)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Define the writer
|
155
|
+
define_method("#{attribute}=") do |value|
|
156
|
+
instance_variable_set("@#{attribute}", value)
|
157
|
+
set_config_value(attribute, value)
|
158
|
+
end
|
159
|
+
|
160
|
+
configuration_method attribute
|
161
|
+
configuration_method "#{attribute}="
|
162
|
+
end
|
163
|
+
|
164
|
+
def configuration_method(*method_names)
|
165
|
+
config_methods.push(*method_names.map{|n| n.to_sym }).uniq!
|
166
|
+
end
|
167
|
+
|
168
|
+
def nested_configurable(*method_names)
|
169
|
+
nested_configurables.push(*method_names)
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
class ConfigurationProxy
|
175
|
+
|
176
|
+
def initialize(owner)
|
177
|
+
@owner = owner
|
178
|
+
end
|
179
|
+
|
180
|
+
def method_missing(method_name, *args, &block)
|
181
|
+
if owner.has_config_method?(method_name)
|
182
|
+
attribute = method_name.to_s.tr('=','').to_sym
|
183
|
+
if method_name.to_s =~ /=$/ && owner.has_config_method?(attribute) # a bit hacky - if it has both getter and setter, assume it's a configurable_attr
|
184
|
+
owner.set_config_value(attribute, args.first)
|
185
|
+
else
|
186
|
+
owner.send(method_name, *args, &block)
|
187
|
+
end
|
188
|
+
elsif nested_configurable?(method_name)
|
189
|
+
owner.send(method_name)
|
190
|
+
else
|
191
|
+
raise BadConfigAttribute, "You tried to configure using '#{method_name.inspect}', but the valid config attributes are #{owner.config_methods.map{|a| %('#{a.inspect}') }.sort.join(', ')}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
attr_reader :owner
|
198
|
+
|
199
|
+
def nested_configurable?(method)
|
200
|
+
owner.class.nested_configurables.include?(method.to_sym)
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module ImageResizer
|
4
|
+
module Loggable
|
5
|
+
|
6
|
+
def log
|
7
|
+
case @log_object
|
8
|
+
when nil
|
9
|
+
@log_object = Logger.new($stdout)
|
10
|
+
when Proc
|
11
|
+
@log_object[]
|
12
|
+
when Logger
|
13
|
+
@log_object
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def log=(object)
|
18
|
+
@log_object = object
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :log_object
|
22
|
+
|
23
|
+
def use_same_log_as(object)
|
24
|
+
self.log = proc{ object.log }
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module ImageResizer
|
5
|
+
class Processor
|
6
|
+
|
7
|
+
include Configurable
|
8
|
+
include Shell
|
9
|
+
include Loggable
|
10
|
+
|
11
|
+
def process(input_path, viewport_size = {}, interest_point = {})
|
12
|
+
input_properties = fetch_image_properties(input_path)
|
13
|
+
input_has_alpha = (input_properties["bands"] == 4)
|
14
|
+
|
15
|
+
output_tmp = Tempfile.new(["img", input_has_alpha ? ".png" : ".jpg"])
|
16
|
+
|
17
|
+
transform = calculate_transform(input_path, input_properties, viewport_size, interest_point)
|
18
|
+
run_transform(input_path, output_tmp.path, transform)
|
19
|
+
|
20
|
+
return output_tmp.path
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_image_properties(input_path)
|
24
|
+
# read in the image headers to discover the width and height of the image.
|
25
|
+
# There's actually some extra metadata we ignore here, but this seems to be
|
26
|
+
# the only way to get width and height from VIPS.
|
27
|
+
result = run("vips im_printdesc '#{input_path}'")
|
28
|
+
|
29
|
+
# for some reason, the response isn't just YAML. It's one line of text in parenthesis
|
30
|
+
# followed by YAML. Let's chop off the first line to get to the good stuff.
|
31
|
+
result = result[(result.index(')') + 2)..-1]
|
32
|
+
return YAML::load(result)
|
33
|
+
end
|
34
|
+
|
35
|
+
def calculate_transform(input_path, input_properties, viewport_size, interest_point)
|
36
|
+
|
37
|
+
# Pull out the properties we're interested in
|
38
|
+
image_size = {w: input_properties["width"].to_f, h: input_properties["height"].to_f}
|
39
|
+
|
40
|
+
# By default, the interest size is 30% of the total image size.
|
41
|
+
# In the future, this could be a parameter, and you'd pass the # of pixels around
|
42
|
+
# the POI you are interested in.
|
43
|
+
if (interest_point[:xf])
|
44
|
+
interest_point[:x] = image_size[:w] * interest_point[:xf]
|
45
|
+
end
|
46
|
+
|
47
|
+
if (interest_point[:yf])
|
48
|
+
interest_point[:y] = image_size[:h] * interest_point[:yf]
|
49
|
+
end
|
50
|
+
|
51
|
+
if (interest_point[:region] == nil)
|
52
|
+
interest_point[:region] = 1
|
53
|
+
end
|
54
|
+
|
55
|
+
if (interest_point[:x] == nil)
|
56
|
+
interest_point[:x] = image_size[:w] * 0.5
|
57
|
+
interest_point[:region] = 1
|
58
|
+
end
|
59
|
+
if (interest_point[:y] == nil)
|
60
|
+
interest_point[:y] = image_size[:h] * 0.5
|
61
|
+
interest_point[:region] = 1
|
62
|
+
end
|
63
|
+
|
64
|
+
interest_size = {w: image_size[:w] * interest_point[:region], h: image_size[:h] * interest_point[:region]}
|
65
|
+
|
66
|
+
# Has the user specified both the width and the height of the viewport? If they haven't,
|
67
|
+
# let's go ahead and fill in the missing properties for them so that they get output at
|
68
|
+
# the original aspect ratio of the image.
|
69
|
+
if ((viewport_size[:w] == nil) && (viewport_size[:h] == nil))
|
70
|
+
viewport_size = {w: input_properties["width"], h: input_properties["height"]}
|
71
|
+
|
72
|
+
elsif (viewport_size[:w] == nil)
|
73
|
+
viewport_size[:w] = viewport_size[:h] * (input_properties["width"] / input_properties["height"])
|
74
|
+
|
75
|
+
elsif (viewport_size[:h] == nil)
|
76
|
+
viewport_size[:h] = viewport_size[:w] * (input_properties["height"] / input_properties["width"])
|
77
|
+
end
|
78
|
+
|
79
|
+
# how can we take our current image and fit it into the viewport? Time for
|
80
|
+
# some fun math! First, let's determine a scale such that the image fits
|
81
|
+
# within the viewport. There are a few rules we want to apply:
|
82
|
+
# 1) The image should _always_ fill the viewport.
|
83
|
+
# 2) The 1/3 of the image around the interest_point should always be visible.
|
84
|
+
# This means that if we try to cram a massive image into a tiny viewport,
|
85
|
+
# we won't get a simple scale-to-fill. We'll get a more zoomed-in version
|
86
|
+
# showing just the 1/3 around the interest_point.
|
87
|
+
|
88
|
+
scale_to_fill = [viewport_size[:w] / image_size[:w], viewport_size[:h] / image_size[:h]].max
|
89
|
+
|
90
|
+
scale_to_interest = [interest_size[:w] / image_size[:w], interest_size[:h] / image_size[:h]].max
|
91
|
+
|
92
|
+
log.debug("POI: ")
|
93
|
+
log.debug(interest_point)
|
94
|
+
log.debug("Image size: ")
|
95
|
+
log.debug(image_size)
|
96
|
+
log.debug("Requested viewport size: ")
|
97
|
+
log.debug(viewport_size)
|
98
|
+
log.debug("scale_to_fill: %f" % scale_to_fill)
|
99
|
+
log.debug("scale_to_interest: %f" % scale_to_interest)
|
100
|
+
|
101
|
+
|
102
|
+
scale_for_best_region = [scale_to_fill, scale_to_interest].max
|
103
|
+
|
104
|
+
# cool! Now, let's figure out what the content offset within the image should be.
|
105
|
+
# We want to keep the point of interest in view whenever possible. First, let's
|
106
|
+
# compute an optimal frame around the POI:
|
107
|
+
best_region = {x: interest_point[:x].to_f - (image_size[:w] * scale_for_best_region) / 2,
|
108
|
+
y: interest_point[:y].to_f - (image_size[:h] * scale_for_best_region) / 2,
|
109
|
+
w: image_size[:w] * scale_for_best_region,
|
110
|
+
h: image_size[:h] * scale_for_best_region}
|
111
|
+
|
112
|
+
# Up to this point, we've been using 'scale_for_best_region' to be the preferred scale of the image.
|
113
|
+
# So, scale could be 1/3 if we want to show the area around the POI, or 1 if we're fitting a whole image
|
114
|
+
# in a viewport that is exactly the same aspect ratio.
|
115
|
+
|
116
|
+
# The next step is to compute a scale that should be applied to the image to make this desired section of
|
117
|
+
# the image fit within the viewport. This is different from the previous scale—if we wanted to fit 1/3 of
|
118
|
+
# the image in a 100x100 pixel viewport, we computed best_region using that 1/3, and now we need to find
|
119
|
+
# the scale that will fit it into 100px.
|
120
|
+
scale = [scale_to_fill, viewport_size[:w] / best_region[:w], viewport_size[:h] / best_region[:h]].max
|
121
|
+
|
122
|
+
# Next, we scale the best_region so that it is in final coordinates. When we perform the affine transform,
|
123
|
+
# it will SCALE the entire image and then CROP it to a region, so our transform rect needs to be in the
|
124
|
+
# coordinate space of the SCALED image, not the initial image.
|
125
|
+
transform = {}
|
126
|
+
transform[:x] = best_region[:x] * scale
|
127
|
+
transform[:y] = best_region[:y] * scale
|
128
|
+
transform[:w] = best_region[:w] * scale
|
129
|
+
transform[:h] = best_region[:h] * scale
|
130
|
+
transform[:scale] = scale
|
131
|
+
|
132
|
+
# transform now represents the region we'd like to have in the final image. All of it, or part of it, may
|
133
|
+
# not actually be within the bounds of the image! We're about to apply some constraints, but first let's
|
134
|
+
# trim the best_region so that it's the SHAPE of the viewport, not just the SCALE of the viewport. Remember,
|
135
|
+
# since the region is still centered around the POI, we can just trim equally on either the W or H as necessary.
|
136
|
+
transform[:x] -= (viewport_size[:w] - transform[:w]) / 2
|
137
|
+
transform[:y] -= (viewport_size[:h] - transform[:h]) / 2
|
138
|
+
transform[:w] = viewport_size[:w]
|
139
|
+
transform[:h] = viewport_size[:h]
|
140
|
+
|
141
|
+
# alright—now our transform most likely extends beyond the bounds of the image
|
142
|
+
# data. Let's add some constraints that push it within the bounds of the image.
|
143
|
+
if (transform[:x] + transform[:w] > image_size[:w] * scale)
|
144
|
+
transform[:x] = image_size[:w] * scale - transform[:w]
|
145
|
+
end
|
146
|
+
|
147
|
+
if (transform[:y] + transform[:h] > image_size[:h] * scale)
|
148
|
+
transform[:y] = image_size[:h] * scale - transform[:h]
|
149
|
+
end
|
150
|
+
|
151
|
+
if (transform[:x] < 0)
|
152
|
+
transform[:x] = 0.0
|
153
|
+
end
|
154
|
+
|
155
|
+
if (transform[:y] < 0)
|
156
|
+
transform[:y] = 0.0
|
157
|
+
end
|
158
|
+
|
159
|
+
log.debug("The transform properties:")
|
160
|
+
log.debug(transform)
|
161
|
+
|
162
|
+
return transform
|
163
|
+
end
|
164
|
+
|
165
|
+
def run_transform(input_path, output_path, transform)
|
166
|
+
# Call through to VIPS:
|
167
|
+
# int im_affinei(in, out, interpolate, a, b, c, d, dx, dy, x, y, w, h)
|
168
|
+
# The first six params are a transformation matrix. A and D are used for X and Y
|
169
|
+
# scale, the other two are b = Y skew and c = X skew. TX and TY are translations
|
170
|
+
# but don't seem to be used.
|
171
|
+
# The last four params define a rect of the source image that is transformed.
|
172
|
+
log.debug(output_path[-3..-1])
|
173
|
+
|
174
|
+
if ((transform[:scale] < 0.5) && (output_path[-3..-1] == "jpg"))
|
175
|
+
scale = transform[:scale]
|
176
|
+
size = [transform[:w], transform[:h]].max
|
177
|
+
|
178
|
+
transform[:scale] = scale * 2
|
179
|
+
transform[:x] *= 2
|
180
|
+
transform[:y] *= 2
|
181
|
+
transform[:w] *= 2
|
182
|
+
transform[:h] *= 2
|
183
|
+
|
184
|
+
|
185
|
+
log.debug("Using two-pass transform")
|
186
|
+
run("vips im_affinei '#{input_path}' '#{output_path}' bilinear #{transform[:scale]} 0 0 #{transform[:scale]} 0 0 #{transform[:x]} #{transform[:y]} #{transform[:w]} #{transform[:h]}")
|
187
|
+
run("vipsthumbnail -s #{size} --nosharpen -o '%s_thumb.jpg:65' '#{output_path}'")
|
188
|
+
FileUtils.mv(output_path[0..-5]+"_thumb.jpg", output_path)
|
189
|
+
|
190
|
+
else
|
191
|
+
run("vips im_affinei '#{input_path}' '#{output_path}' bilinear #{transform[:scale]} 0 0 #{transform[:scale]} 0 0 #{transform[:x]} #{transform[:y]} #{transform[:w]} #{transform[:h]}")
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
return output_path
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|