insano_image_resizer 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ source :rubygems
2
+
3
+ group :development, :test do
4
+ gem 'rspec'
5
+ gem 'jeweler'
6
+ gem 'pry'
7
+ gem 'pry-nav'
8
+ gem 'pry-stack_explorer'
9
+ end
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
+ ![A brief overview of the Insano processing function](insano_image_resizer/raw/master/samples/explanation.png)
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
+