morandi 0.12.1 → 0.99.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,245 +1,251 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'morandi/profiled_pixbuf'
2
4
  require 'morandi/redeye'
5
+ require 'morandi/operation/straighten'
6
+ require 'morandi/operation/colourify'
7
+ require 'morandi/operation/image_border'
3
8
 
4
- class Morandi::ImageProcessor
5
- attr_reader :options, :pb
6
- attr_accessor :config
7
-
8
- def self.default_icc_path(path)
9
- "#{path}.icc.jpg"
10
- end
9
+ module Morandi
10
+ # rubocop:disable Metrics/ClassLength
11
11
 
12
- def initialize(file, user_options, local_options={})
13
- @file = file
12
+ # ImageProcessor transforms an image.
13
+ class ImageProcessor
14
+ attr_reader :options, :pb
14
15
 
15
- user_options.keys.grep(/^path/).each { |k| user_options.delete(k) }
16
+ def initialize(file, user_options, local_options = {})
17
+ @file = file
16
18
 
17
- # Give priority to user_options
18
- @options = (local_options || {}).merge(user_options || {})
19
- @local_options = local_options
19
+ user_options.keys.grep(/^path/).each { |k| user_options.delete(k) }
20
20
 
21
- @scale_to = @options['output.max']
22
- @width, @height = @options['output.width'], @options['output.height']
21
+ # Give priority to user_options
22
+ @options = (local_options || {}).merge(user_options || {})
23
+ @local_options = local_options
23
24
 
24
- if @file.is_a?(String)
25
- get_pixbuf
26
- elsif @file.is_a?(GdkPixbuf::Pixbuf) or @file.is_a?(Morandi::ProfiledPixbuf)
27
- @pb = @file
28
- @scale = 1.0
25
+ @max_size_px = @options['output.max']
26
+ @width = @options['output.width']
27
+ @height = @options['output.height']
29
28
  end
30
- end
31
29
 
32
- def process!
33
- # Apply Red-Eye corrections
34
- apply_redeye!
30
+ def process!
31
+ case @file
32
+ when String
33
+ get_pixbuf
34
+ when GdkPixbuf::Pixbuf, Morandi::ProfiledPixbuf
35
+ @pb = @file
36
+ @scale = 1.0
37
+ end
38
+
39
+ # Apply Red-Eye corrections
40
+ apply_redeye!
35
41
 
36
- # Apply contrast, brightness etc
37
- apply_colour_manipulations!
42
+ # Apply contrast, brightness etc
43
+ apply_colour_manipulations!
38
44
 
39
- # apply rotation
40
- apply_rotate!
45
+ # apply rotation
46
+ apply_rotate!
41
47
 
42
- # apply crop
43
- apply_crop!
48
+ # apply crop
49
+ apply_crop!
44
50
 
45
- # apply filter
46
- apply_filters!
51
+ # apply filter
52
+ apply_filters!
47
53
 
48
- # add border
49
- apply_decorations!
54
+ # add border
55
+ apply_decorations!
50
56
 
51
- if @options['output.limit'] && @width && @height
52
- @pb = @pb.scale_max([@width, @height].max)
57
+ @pb = @pb.scale_max([@width, @height].max) if @options['output.limit'] && @width && @height
58
+
59
+ @pb
60
+ rescue GdkPixbuf::PixbufError::UnknownType => e
61
+ raise UnknownTypeError, e.message
62
+ rescue GdkPixbuf::PixbufError::CorruptImage => e
63
+ raise CorruptImageError, e.message
53
64
  end
54
65
 
55
- @pb
56
- end
66
+ # Returns generated pixbuf
67
+ def result
68
+ process! unless @pb
69
+ @pb
70
+ end
57
71
 
58
- def result
59
- @pb
60
- end
72
+ def write_to_png(write_to, orientation = :any)
73
+ pb = @pb
61
74
 
62
- def write_to_png(fn, orientation=:any)
63
- pb = @pb
75
+ case orientation
76
+ when :landscape
77
+ pb = @pb.rotate(90) if @pb.width < @pb.height
78
+ when :portrait
79
+ pb = @pb.rotate(90) if @pb.width > @pb.height
80
+ end
81
+ pb.save(write_to, 'png')
82
+ end
64
83
 
65
- case orientation
66
- when :landscape
67
- pb = @pb.rotate(90) if @pb.width < @pb.height
68
- when :portrait
69
- pb = @pb.rotate(90) if @pb.width > @pb.height
84
+ def write_to_jpeg(write_to, quality = nil)
85
+ quality ||= options.fetch('quality', '97')
86
+ @pb.save(write_to, 'jpeg', quality: quality.to_s)
70
87
  end
71
- pb.save(fn, 'png')
72
- end
73
88
 
74
- def write_to_jpeg(fn, quality = nil)
75
- quality ||= options.fetch('quality', '97')
76
- @pb.save(fn, 'jpeg', quality: quality.to_s)
77
- end
89
+ protected
78
90
 
79
- protected
80
- def get_pixbuf
81
- _, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
82
-
83
- if @scale_to
84
- @pb = Morandi::ProfiledPixbuf.new(@file, @scale_to, @scale_to, @local_options)
85
- @src_max = [width, height].max
86
- @actual_max = [@pb.width, @pb.height].max
87
- else
88
- @pb = Morandi::ProfiledPixbuf.new(@file, @local_options)
89
- @src_max = [@pb.width, @pb.height].max
90
- @actual_max = [@pb.width, @pb.height].max
91
- end
91
+ def get_pixbuf
92
+ _, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
93
+ @pb = Morandi::ProfiledPixbuf.new(@file, @local_options, @max_size_px)
92
94
 
93
- @scale = @actual_max / @src_max.to_f
94
- end
95
+ # Everything below probably could be substituted with the following:
96
+ # @scale = @max_size_px ? @max_size_px / [width, height].max : 1.0
97
+ actual_max = [@pb.width, @pb.height].max
98
+ src_max = if @max_size_px
99
+ [width, height].max
100
+ else
101
+ [@pb.width, @pb.height].max
102
+ end
95
103
 
96
- SHARPEN = [
97
- -1, -1, -1, -1, -1,
98
- -1, 2, 2, 2, -1,
99
- -1, 2, 8, 2, -1,
100
- -1, 2, 2, 2, -1,
101
- -1, -1, -1, -1, -1,
102
- ]
103
- BLUR = [
104
- 0, 1, 1, 1, 0,
105
- 1, 1, 1, 1, 1,
106
- 1, 1, 1, 1, 1,
107
- 1, 1, 1, 1, 1,
108
- 0, 1, 1, 1, 0,
109
- ]
110
-
111
- def apply_colour_manipulations!
112
- if options['brighten'].to_i.nonzero?
113
- brighten = [ [ 5 * options['brighten'], -100 ].max, 100 ].min
114
- @pb = PixbufUtils.brightness(@pb, brighten)
104
+ @scale = actual_max / src_max.to_f
115
105
  end
116
106
 
117
- if options['gamma'] && (options['gamma'] != 1.0)
118
- @pb = PixbufUtils.gamma(@pb, options['gamma'])
119
- end
107
+ SHARPEN = [
108
+ -1, -1, -1, -1, -1,
109
+ -1, 2, 2, 2, -1,
110
+ -1, 2, 8, 2, -1,
111
+ -1, 2, 2, 2, -1,
112
+ -1, -1, -1, -1, -1
113
+ ].freeze
120
114
 
121
- if options['contrast'].to_i.nonzero?
122
- @pb = PixbufUtils.contrast(@pb, [ [ 5 * options['contrast'], -100 ].max, 100 ].min)
123
- end
115
+ BLUR = [
116
+ 0, 1, 1, 1, 0,
117
+ 1, 1, 1, 1, 1,
118
+ 1, 1, 1, 1, 1,
119
+ 1, 1, 1, 1, 1,
120
+ 0, 1, 1, 1, 0
121
+ ].freeze
122
+
123
+ def apply_colour_manipulations!
124
+ if options['brighten'].to_i.nonzero?
125
+ brighten = (5 * options['brighten']).clamp(-100, 100)
126
+ @pb = MorandiNative::PixbufUtils.brightness(@pb, brighten)
127
+ end
128
+
129
+ if options['gamma'] && not_equal_to_one(options['gamma'])
130
+ @pb = MorandiNative::PixbufUtils.gamma(@pb,
131
+ options['gamma'])
132
+ end
133
+
134
+ if options['contrast'].to_i.nonzero?
135
+ contrast = (5 * options['contrast']).clamp(-100, 100)
136
+ @pb = MorandiNative::PixbufUtils.contrast(@pb, contrast)
137
+ end
138
+
139
+ return unless options['sharpen'].to_i.nonzero?
124
140
 
125
- if options['sharpen'].to_i.nonzero?
126
- if options['sharpen'] > 0
141
+ if options['sharpen'].positive?
127
142
  [options['sharpen'], 5].min.times do
128
- @pb = PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
143
+ @pb = MorandiNative::PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
129
144
  end
130
- elsif options['sharpen'] < 0
131
- [ (options['sharpen']*-1), 5].min.times do
132
- @pb = PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
145
+ elsif options['sharpen'].negative?
146
+ [(options['sharpen'] * -1), 5].min.times do
147
+ @pb = MorandiNative::PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
133
148
  end
134
149
  end
135
150
  end
136
- end
137
151
 
138
- def apply_redeye!
139
- for eye in options['redeye'] || []
140
- @pb = Morandi::RedEye::TapRedEye.tap_on(@pb, eye[0] * @scale, eye[1] * @scale)
152
+ def apply_redeye!
153
+ (options['redeye'] || []).each do |eye|
154
+ @pb = Morandi::RedEye::TapRedEye.tap_on(@pb, eye[0] * @scale, eye[1] * @scale)
155
+ end
141
156
  end
142
- end
143
157
 
144
- def angle
145
- a = options['angle'].to_i
146
- if a
147
- (360-a)%360
148
- else
149
- nil
158
+ def angle
159
+ a = options['angle'].to_i
160
+ (360 - a) % 360 if a
150
161
  end
151
- end
152
162
 
153
- # modifies @pb with any applied rotation
154
- def apply_rotate!
155
- a = angle()
163
+ # modifies @pb with any applied rotation
164
+ def apply_rotate!
165
+ a = angle
156
166
 
157
- unless (a%360).zero?
158
- @pb = @pb.rotate(a)
167
+ @pb = @pb.rotate(a) unless (a % 360).zero?
168
+
169
+ unless options['straighten'].to_f.zero?
170
+ @pb = Morandi::Operation::Straighten.new_from_hash(angle: options['straighten'].to_f).call(@pb)
171
+ end
172
+
173
+ @image_width = @pb.width
174
+ @image_height = @pb.height
159
175
  end
160
176
 
161
- unless options['straighten'].to_f.zero?
162
- @pb = Morandi::Straighten.new(options['straighten'].to_f).call(nil, @pb)
177
+ DEFAULT_CONFIG = {
178
+ 'border-size-mm' => 5
179
+ }.freeze
180
+ def config_for(key)
181
+ return options[key] if options&.key?(key)
182
+
183
+ DEFAULT_CONFIG[key]
163
184
  end
164
185
 
165
- @image_width = @pb.width
166
- @image_height = @pb.height
167
- end
186
+ def apply_crop!
187
+ crop = options['crop']
168
188
 
169
- DEFAULT_CONFIG = {
170
- 'border-size-mm' => 5
171
- }
172
- def config_for(key)
173
- return options[key] if options && options.has_key?(key)
174
- return @config[key] if @config && @config.has_key?(key)
175
- DEFAULT_CONFIG[key]
176
- end
189
+ return if crop.nil? && config_for('image.auto-crop').eql?(false)
177
190
 
178
- #
179
- def apply_crop!
180
- crop = options['crop']
191
+ crop = crop.split(',').map(&:to_i) if crop.is_a?(String) && crop =~ /^\d+,\d+,\d+,\d+/
181
192
 
182
- if crop.nil? && config_for('image.auto-crop').eql?(false)
183
- return
184
- end
193
+ crop = nil unless crop.is_a?(Array) && crop.size.eql?(4) && crop.all? do |i|
194
+ i.is_a?(Numeric)
195
+ end
185
196
 
186
- if crop.is_a?(String) && crop =~ /^\d+,\d+,\d+,\d+/
187
- crop = crop.split(/,/).map(&:to_i)
188
- end
197
+ # can't crop, won't crop
198
+ return if @width.nil? && @height.nil? && crop.nil?
189
199
 
190
- crop = nil unless crop.is_a?(Array) && crop.size.eql?(4) && crop.all? { |i|
191
- i.kind_of?(Numeric)
192
- }
200
+ crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
193
201
 
194
- # can't crop, won't crop
195
- return if @width.nil? && @height.nil? && crop.nil?
202
+ crop ||= Morandi::CropUtils.autocrop_coords(@pb.width, @pb.height, @width, @height)
196
203
 
197
- if crop && @scale != 1.0
198
- crop = crop.map { |s| (s.to_f * @scale).floor }
204
+ @pb = Morandi::CropUtils.apply_crop(@pb, crop[0], crop[1], crop[2], crop[3])
199
205
  end
200
206
 
201
- crop ||= Morandi::Utils.autocrop_coords(@pb.width, @pb.height, @width, @height)
207
+ def apply_filters!
208
+ filter = options['fx']
202
209
 
203
- @pb = Morandi::Utils.apply_crop(@pb, crop[0], crop[1], crop[2], crop[3])
204
- end
210
+ case filter
211
+ when 'greyscale', 'sepia', 'bluetone'
212
+ op = Morandi::Operation::Colourify.new_from_hash('filter' => filter)
213
+ else
214
+ return
215
+ end
216
+ @pb = op.call(@pb)
217
+ end
205
218
 
219
+ def apply_decorations!
220
+ style = options['border-style']
221
+ colour = options['background-style']
206
222
 
207
- def apply_filters!
208
- filter = options['fx']
223
+ return if style.nil? || style.eql?('none')
224
+ return if colour.eql?('none')
209
225
 
210
- case filter
211
- when 'greyscale'
212
- op = Morandi::Colourify.new_from_hash('op' => filter)
213
- when 'sepia', 'bluetone'
214
- # could also set 'alpha' => (0.85 * 255).to_i
215
- op = Morandi::Colourify.new_from_hash('op' => filter)
216
- else
217
- return
218
- end
219
- @pb = op.call(nil, @pb)
220
- end
226
+ colour ||= 'black'
221
227
 
222
- def apply_decorations!
223
- style, colour = options['border-style'], options['background-style']
228
+ crop = options['crop']
229
+ crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
224
230
 
225
- return if style.nil? or style.eql?('none')
226
- return if colour.eql?('none')
227
- colour ||= 'black'
231
+ op = Morandi::Operation::ImageBorder.new_from_hash(
232
+ 'style' => style,
233
+ 'colour' => colour || '#000000',
234
+ 'crop' => crop,
235
+ 'size' => [@image_width, @image_height],
236
+ 'print_size' => [@width, @height],
237
+ 'shrink' => true,
238
+ 'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
239
+ )
228
240
 
229
- crop = options['crop']
230
- crop = crop.map { |s| (s.to_f * @scale).floor } if crop && @scale != 1.0
241
+ @pb = op.call(@pb)
242
+ end
231
243
 
232
- op = Morandi::ImageBorder.new_from_hash(data={
233
- 'style' => style,
234
- 'colour' => colour || '#000000',
235
- 'crop' => crop,
236
- 'size' => [@image_width, @image_height],
237
- 'print_size' => [@width, @height],
238
- 'shrink' => true,
239
- 'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
240
- })
244
+ private
241
245
 
242
- @pb = op.call(nil, @pb)
246
+ def not_equal_to_one(float)
247
+ (float - 1.0).abs >= Float::EPSILON
248
+ end
243
249
  end
244
-
250
+ # rubocop:enable Metrics/ClassLength
245
251
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'morandi/image_operation'
4
+
5
+ module Morandi
6
+ module Operation
7
+ # Colourify Operation
8
+ # Apply tint to image with variable strength
9
+ # Supports filter, alpha
10
+ class Colourify < ImageOperation
11
+ attr_reader :filter
12
+
13
+ def alpha
14
+ @alpha || 255
15
+ end
16
+
17
+ def sepia(pixbuf)
18
+ MorandiNative::PixbufUtils.tint(pixbuf, 25, 5, -25, alpha)
19
+ end
20
+
21
+ def bluetone(pixbuf)
22
+ MorandiNative::PixbufUtils.tint(pixbuf, -10, 5, 25, alpha)
23
+ end
24
+
25
+ def null(pixbuf)
26
+ pixbuf
27
+ end
28
+ alias full null # WebKiosk
29
+ alias colour null # WebKiosk
30
+
31
+ def greyscale(pixbuf)
32
+ MorandiNative::PixbufUtils.tint(pixbuf, 0, 0, 0, alpha)
33
+ end
34
+ alias bw greyscale # WebKiosk
35
+
36
+ def call(pixbuf)
37
+ if @filter && respond_to?(@filter)
38
+ __send__(@filter, pixbuf)
39
+ else
40
+ pixbuf # Default is nothing
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorscore'
4
+ require 'morandi/image_operation'
5
+
6
+ module Morandi
7
+ module Operation
8
+ # Image Border operation
9
+ # Supports retro (rounded) and square borders
10
+ # Background colour (ie. border colour) can be white, black, dominant (ie. from image)
11
+ # @!visibility private
12
+ class ImageBorder < ImageOperation
13
+ attr_accessor :style, :colour, :crop, :size, :print_size, :shrink, :border_size
14
+
15
+ def call(pixbuf)
16
+ return pixbuf unless %w[square retro].include? @style
17
+
18
+ create_pixbuf_from_image_surface(:rgb24, pixbuf.width, pixbuf.height) do |cr|
19
+ if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
20
+ img_width = size[0]
21
+ img_height = size[1]
22
+ else
23
+ img_width = pixbuf.width
24
+ img_height = pixbuf.height
25
+ end
26
+
27
+ @border_scale = [img_width, img_height].max.to_f / print_size.max.to_i
28
+
29
+ draw_background(cr, img_height, img_width, pixbuf)
30
+
31
+ x = border_width
32
+ y = border_width
33
+
34
+ # This biggest impact will be on the smallest side, so to avoid white
35
+ # edges between photo and border scale by the longest changed side.
36
+ longest_side = [pixbuf.width, pixbuf.height].max.to_f
37
+
38
+ # Should be less than 1
39
+ pb_scale = (longest_side - (border_width * 2)) / longest_side
40
+
41
+ if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
42
+ x -= @crop[0]
43
+ y -= @crop[1]
44
+ end
45
+
46
+ draw_pixbuf(pixbuf, cr, img_height, img_width, pb_scale, x, y)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # Width is proportional to output size
53
+ def border_width
54
+ @border_size * @border_scale
55
+ end
56
+
57
+ def draw_pixbuf(pixbuf, cr, img_height, img_width, pb_scale, x, y)
58
+ case style
59
+ when 'retro'
60
+ Morandi::CairoExt.rounded_rectangle(cr, x, y,
61
+ img_width + x - (border_width * 2),
62
+ img_height + y - (border_width * 2), border_width)
63
+ when 'square'
64
+ cr.rectangle(x, y, img_width - (border_width * 2), img_height - (border_width * 2))
65
+ end
66
+ cr.clip
67
+
68
+ if @shrink
69
+ cr.translate(border_width, border_width)
70
+ cr.scale(pb_scale, pb_scale)
71
+ end
72
+ cr.set_source_pixbuf(pixbuf)
73
+ cr.rectangle(0, 0, pixbuf.width, pixbuf.height)
74
+
75
+ cr.paint(1.0)
76
+ end
77
+
78
+ def draw_background(cr, img_height, img_width, pixbuf)
79
+ cr.save do
80
+ cr.translate(-@crop[0], -@crop[1]) if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
81
+
82
+ cr.save do
83
+ cr.set_operator :source
84
+ cr.set_source_rgb 1, 1, 1
85
+ cr.paint
86
+
87
+ cr.rectangle(0, 0, img_width, img_height)
88
+ case colour
89
+ when 'dominant'
90
+ pixbuf.scale_max(400).save(fn = "/tmp/hist-#{$PROCESS_ID}.#{Time.now.to_i}", 'jpeg')
91
+ histogram = Colorscore::Histogram.new(fn)
92
+ FileUtils.rm_f(fn)
93
+ col = histogram.scores.first[1]
94
+ cr.set_source_rgb col.red / 256.0, col.green / 256.0, col.blue / 256.0
95
+ when 'retro'
96
+ cr.set_source_rgb 1, 1, 0.8
97
+ when 'black'
98
+ cr.set_source_rgb 0, 0, 0
99
+ else
100
+ cr.set_source_rgb 1, 1, 1
101
+ end
102
+ cr.fill
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'morandi/image_operation'
4
+
5
+ module Morandi
6
+ module Operation
7
+ # Straighten operation
8
+ # Does a small (ie. not 90,180,270 deg) rotation and zooms to avoid cropping
9
+ # @!visibility private
10
+ class Straighten < ImageOperation
11
+ attr_accessor :angle
12
+
13
+ def call(pixbuf)
14
+ return pixbuf if angle.zero?
15
+
16
+ rotation_value_rad = angle * (Math::PI / 180)
17
+
18
+ ratio = pixbuf.width.to_f / pixbuf.height
19
+ rh = pixbuf.height / ((ratio * Math.sin(rotation_value_rad.abs)) + Math.cos(rotation_value_rad.abs))
20
+ scale = pixbuf.height / rh.to_f.abs
21
+
22
+ a_ratio = pixbuf.height.to_f / pixbuf.width
23
+ a_rh = pixbuf.width / ((a_ratio * Math.sin(rotation_value_rad.abs)) + Math.cos(rotation_value_rad.abs))
24
+ a_scale = pixbuf.width / a_rh.to_f.abs
25
+
26
+ scale = a_scale if a_scale > scale
27
+
28
+ create_pixbuf_from_image_surface(:rgb24, pixbuf.width, pixbuf.height) do |cr|
29
+ cr.translate(pixbuf.width / 2.0, pixbuf.height / 2.0)
30
+ cr.rotate(rotation_value_rad)
31
+ cr.scale(scale, scale)
32
+ cr.translate(pixbuf.width / -2.0, pixbuf.height / - 2.0)
33
+ cr.set_source_pixbuf(pixbuf)
34
+
35
+ cr.rectangle(0, 0, pixbuf.width, pixbuf.height)
36
+ cr.paint(1.0)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gdk_pixbuf2'
4
+
5
+ # GdkPixbuf module / hierachy
6
+ module GdkPixbuf
7
+ # Add #to_cairo_image_surface for converting pixels to Cairo::ImageSurface format (RGBA->ARGB)
8
+ class Pixbuf
9
+ def to_cairo_image_surface
10
+ GdkPixbufCairo.pixbuf_to_surface(self)
11
+ end
12
+
13
+ # Proportionally scales down the image so that it fits within max_size*max_size square
14
+ def scale_max(max_size, interp = GdkPixbuf::InterpType::BILINEAR, _max_scale = 1.0)
15
+ mul = (max_size / [width, height].max.to_f)
16
+ mul = [1.0, mul].min
17
+ scale(width * mul, height * mul, interp)
18
+ end
19
+ end
20
+ end