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