morandi 0.12.1 → 0.99.4

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.
@@ -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