morandi 0.12.0 → 0.99.03

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,245 +1,245 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'morandi/profiled_pixbuf'
2
4
  require 'morandi/redeye'
3
5
 
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
6
+ module Morandi
7
+ # rubocop:disable Metrics/ClassLength
11
8
 
12
- def initialize(file, user_options, local_options={})
13
- @file = file
9
+ # ImageProcessor transforms an image.
10
+ class ImageProcessor
11
+ attr_reader :options, :pb
12
+ attr_accessor :config
14
13
 
15
- user_options.keys.grep(/^path/).each { |k| user_options.delete(k) }
14
+ def initialize(file, user_options, local_options = {})
15
+ @file = file
16
16
 
17
- # Give priority to user_options
18
- @options = (local_options || {}).merge(user_options || {})
19
- @local_options = local_options
17
+ user_options.keys.grep(/^path/).each { |k| user_options.delete(k) }
20
18
 
21
- @scale_to = @options['output.max']
22
- @width, @height = @options['output.width'], @options['output.height']
19
+ # Give priority to user_options
20
+ @options = (local_options || {}).merge(user_options || {})
21
+ @local_options = local_options
23
22
 
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
23
+ @scale_to = @options['output.max']
24
+ @width = @options['output.width']
25
+ @height = @options['output.height']
29
26
  end
30
- end
31
27
 
32
- def process!
33
- # Apply Red-Eye corrections
34
- apply_redeye!
28
+ def process!
29
+ case @file
30
+ when String
31
+ get_pixbuf
32
+ when GdkPixbuf::Pixbuf, Morandi::ProfiledPixbuf
33
+ @pb = @file
34
+ @scale = 1.0
35
+ end
35
36
 
36
- # Apply contrast, brightness etc
37
- apply_colour_manipulations!
37
+ # Apply Red-Eye corrections
38
+ apply_redeye!
38
39
 
39
- # apply rotation
40
- apply_rotate!
40
+ # Apply contrast, brightness etc
41
+ apply_colour_manipulations!
41
42
 
42
- # apply crop
43
- apply_crop!
43
+ # apply rotation
44
+ apply_rotate!
44
45
 
45
- # apply filter
46
- apply_filters!
46
+ # apply crop
47
+ apply_crop!
47
48
 
48
- # add border
49
- apply_decorations!
49
+ # apply filter
50
+ apply_filters!
50
51
 
51
- if @options['output.limit'] && @width && @height
52
- @pb = @pb.scale_max([@width, @height].max)
53
- end
52
+ # add border
53
+ apply_decorations!
54
54
 
55
- @pb
56
- end
55
+ @pb = @pb.scale_max([@width, @height].max) if @options['output.limit'] && @width && @height
57
56
 
58
- def result
59
- @pb
60
- end
57
+ @pb
58
+ end
61
59
 
62
- def write_to_png(fn, orientation=:any)
63
- pb = @pb
60
+ # Returns generated pixbuf
61
+ def result
62
+ process! unless @pb
63
+ @pb
64
+ end
65
+
66
+ def write_to_png(write_to, orientation = :any)
67
+ pb = @pb
64
68
 
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
69
+ case orientation
70
+ when :landscape
71
+ pb = @pb.rotate(90) if @pb.width < @pb.height
72
+ when :portrait
73
+ pb = @pb.rotate(90) if @pb.width > @pb.height
74
+ end
75
+ pb.save(write_to, 'png')
70
76
  end
71
- pb.save(fn, 'png')
72
- end
73
77
 
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
78
+ def write_to_jpeg(write_to, quality = nil)
79
+ quality ||= options.fetch('quality', '97')
80
+ @pb.save(write_to, 'jpeg', quality: quality.to_s)
81
+ end
78
82
 
79
- protected
80
- def get_pixbuf
81
- _, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
83
+ protected
82
84
 
83
- if @scale_to
84
- @pb = Morandi::ProfiledPixbuf.new(@file, @scale_to, @scale_to, @local_options)
85
- @src_max = [width, height].max
85
+ def get_pixbuf
86
+ _, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
87
+ @pb = Morandi::ProfiledPixbuf.new(@file, @local_options, @scale_to)
86
88
  @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
92
89
 
93
- @scale = @actual_max / @src_max.to_f
94
- end
90
+ @src_max = if @scale_to
91
+ [width, height].max
92
+ else
93
+ [@pb.width, @pb.height].max
94
+ end
95
+
96
+ @scale = @actual_max / @src_max.to_f
97
+ end
98
+
99
+ SHARPEN = [
100
+ -1, -1, -1, -1, -1,
101
+ -1, 2, 2, 2, -1,
102
+ -1, 2, 8, 2, -1,
103
+ -1, 2, 2, 2, -1,
104
+ -1, -1, -1, -1, -1
105
+ ].freeze
106
+
107
+ BLUR = [
108
+ 0, 1, 1, 1, 0,
109
+ 1, 1, 1, 1, 1,
110
+ 1, 1, 1, 1, 1,
111
+ 1, 1, 1, 1, 1,
112
+ 0, 1, 1, 1, 0
113
+ ].freeze
114
+
115
+ def apply_colour_manipulations!
116
+ if options['brighten'].to_i.nonzero?
117
+ brighten = (5 * options['brighten']).clamp(-100, 100)
118
+ @pb = MorandiNative::PixbufUtils.brightness(@pb, brighten)
119
+ end
95
120
 
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)
115
- end
121
+ if options['gamma'] && not_equal_to_one(options['gamma'])
122
+ @pb = MorandiNative::PixbufUtils.gamma(@pb,
123
+ options['gamma'])
124
+ end
116
125
 
117
- if options['gamma'] && (options['gamma'] != 1.0)
118
- @pb = PixbufUtils.gamma(@pb, options['gamma'])
119
- end
126
+ if options['contrast'].to_i.nonzero?
127
+ contrast = (5 * options['contrast']).clamp(-100, 100)
128
+ @pb = MorandiNative::PixbufUtils.contrast(@pb, contrast)
129
+ end
120
130
 
121
- if options['contrast'].to_i.nonzero?
122
- @pb = PixbufUtils.contrast(@pb, [ [ 5 * options['contrast'], -100 ].max, 100 ].min)
123
- end
131
+ return unless options['sharpen'].to_i.nonzero?
124
132
 
125
- if options['sharpen'].to_i.nonzero?
126
- if options['sharpen'] > 0
133
+ if options['sharpen'].positive?
127
134
  [options['sharpen'], 5].min.times do
128
- @pb = PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
135
+ @pb = MorandiNative::PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
129
136
  end
130
- elsif options['sharpen'] < 0
131
- [ (options['sharpen']*-1), 5].min.times do
132
- @pb = PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
137
+ elsif options['sharpen'].negative?
138
+ [(options['sharpen'] * -1), 5].min.times do
139
+ @pb = MorandiNative::PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
133
140
  end
134
141
  end
135
142
  end
136
- end
137
143
 
138
- def apply_redeye!
139
- for eye in options['redeye'] || []
140
- @pb = Morandi::RedEye::TapRedEye.tap_on(@pb, eye[0] * @scale, eye[1] * @scale)
144
+ def apply_redeye!
145
+ (options['redeye'] || []).each do |eye|
146
+ @pb = Morandi::RedEye::TapRedEye.tap_on(@pb, eye[0] * @scale, eye[1] * @scale)
147
+ end
141
148
  end
142
- end
143
149
 
144
- def angle
145
- a = options['angle'].to_i
146
- if a
147
- (360-a)%360
148
- else
149
- nil
150
+ def angle
151
+ a = options['angle'].to_i
152
+ (360 - a) % 360 if a
150
153
  end
151
- end
152
154
 
153
- # modifies @pb with any applied rotation
154
- def apply_rotate!
155
- a = angle()
155
+ # modifies @pb with any applied rotation
156
+ def apply_rotate!
157
+ a = angle
158
+
159
+ @pb = @pb.rotate(a) unless (a % 360).zero?
156
160
 
157
- unless (a%360).zero?
158
- @pb = @pb.rotate(a)
161
+ unless options['straighten'].to_f.zero?
162
+ @pb = Morandi::Straighten.new_from_hash(angle: options['straighten'].to_f).call(nil,
163
+ @pb)
164
+ end
165
+
166
+ @image_width = @pb.width
167
+ @image_height = @pb.height
159
168
  end
160
169
 
161
- unless options['straighten'].to_f.zero?
162
- @pb = Morandi::Straighten.new(options['straighten'].to_f).call(nil, @pb)
170
+ DEFAULT_CONFIG = {
171
+ 'border-size-mm' => 5
172
+ }.freeze
173
+ def config_for(key)
174
+ return options[key] if options&.key?(key)
175
+ return @config[key] if @config&.key?(key)
176
+
177
+ DEFAULT_CONFIG[key]
163
178
  end
164
179
 
165
- @image_width = @pb.width
166
- @image_height = @pb.height
167
- end
180
+ def apply_crop!
181
+ crop = options['crop']
168
182
 
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
183
+ return if crop.nil? && config_for('image.auto-crop').eql?(false)
177
184
 
178
- #
179
- def apply_crop!
180
- crop = options['crop']
185
+ crop = crop.split(',').map(&:to_i) if crop.is_a?(String) && crop =~ /^\d+,\d+,\d+,\d+/
181
186
 
182
- if crop.nil? && config_for('image.auto-crop').eql?(false)
183
- return
184
- end
187
+ crop = nil unless crop.is_a?(Array) && crop.size.eql?(4) && crop.all? do |i|
188
+ i.is_a?(Numeric)
189
+ end
185
190
 
186
- if crop.is_a?(String) && crop =~ /^\d+,\d+,\d+,\d+/
187
- crop = crop.split(/,/).map(&:to_i)
188
- end
191
+ # can't crop, won't crop
192
+ return if @width.nil? && @height.nil? && crop.nil?
189
193
 
190
- crop = nil unless crop.is_a?(Array) && crop.size.eql?(4) && crop.all? { |i|
191
- i.kind_of?(Numeric)
192
- }
194
+ crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
193
195
 
194
- # can't crop, won't crop
195
- return if @width.nil? && @height.nil? && crop.nil?
196
+ crop ||= Morandi::CropUtils.autocrop_coords(@pb.width, @pb.height, @width, @height)
196
197
 
197
- if crop && @scale != 1.0
198
- crop = crop.map { |s| (s.to_f * @scale).floor }
198
+ @pb = Morandi::CropUtils.apply_crop(@pb, crop[0], crop[1], crop[2], crop[3])
199
199
  end
200
200
 
201
- crop ||= Morandi::Utils.autocrop_coords(@pb.width, @pb.height, @width, @height)
201
+ def apply_filters!
202
+ filter = options['fx']
202
203
 
203
- @pb = Morandi::Utils.apply_crop(@pb, crop[0], crop[1], crop[2], crop[3])
204
- end
204
+ case filter
205
+ when 'greyscale', 'sepia', 'bluetone'
206
+ op = Morandi::Colourify.new_from_hash('filter' => filter)
207
+ else
208
+ return
209
+ end
210
+ @pb = op.call(nil, @pb)
211
+ end
205
212
 
213
+ def apply_decorations!
214
+ style = options['border-style']
215
+ colour = options['background-style']
206
216
 
207
- def apply_filters!
208
- filter = options['fx']
217
+ return if style.nil? || style.eql?('none')
218
+ return if colour.eql?('none')
209
219
 
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
220
+ colour ||= 'black'
221
221
 
222
- def apply_decorations!
223
- style, colour = options['border-style'], options['background-style']
222
+ crop = options['crop']
223
+ crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
224
224
 
225
- return if style.nil? or style.eql?('none')
226
- return if colour.eql?('none')
227
- colour ||= 'black'
225
+ op = Morandi::ImageBorder.new_from_hash(
226
+ 'style' => style,
227
+ 'colour' => colour || '#000000',
228
+ 'crop' => crop,
229
+ 'size' => [@image_width, @image_height],
230
+ 'print_size' => [@width, @height],
231
+ 'shrink' => true,
232
+ 'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
233
+ )
228
234
 
229
- crop = options['crop']
230
- crop = crop.map { |s| (s.to_f * @scale).floor } if crop && @scale != 1.0
235
+ @pb = op.call(nil, @pb)
236
+ end
231
237
 
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
- })
238
+ private
241
239
 
242
- @pb = op.call(nil, @pb)
240
+ def not_equal_to_one(float)
241
+ (float - 1.0).abs >= Float::EPSILON
242
+ end
243
243
  end
244
-
244
+ # rubocop:enable Metrics/ClassLength
245
245
  end
@@ -0,0 +1,19 @@
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
+ def scale_max(max_size, interp = GdkPixbuf::InterpType::BILINEAR, _max_scale = 1.0)
14
+ mul = (max_size / [width, height].max.to_f)
15
+ mul = [1.0, mul].min
16
+ scale(width * mul, height * mul, interp)
17
+ end
18
+ end
19
+ end
@@ -1,82 +1,61 @@
1
- require 'gdk_pixbuf2'
1
+ # frozen_string_literal: true
2
2
 
3
- class Morandi::ProfiledPixbuf < GdkPixbuf::Pixbuf
4
- def valid_jpeg?(filename)
5
- return false unless File.exist?(filename)
6
- return false unless File.size(filename) > 0
3
+ require 'gdk_pixbuf2'
7
4
 
8
- type, _, _ = GdkPixbuf::Pixbuf.get_file_info(filename)
5
+ module Morandi
6
+ # ProfiledPixbuf is a descendent of GdkPixbuf::Pixbuf with ICC support.
7
+ # It attempts to load an image using jpegicc/littlecms to ensure that it is sRGB.
8
+ class ProfiledPixbuf < GdkPixbuf::Pixbuf
9
+ def valid_jpeg?(filename)
10
+ return false unless File.exist?(filename)
11
+ return false unless File.size(filename).positive?
9
12
 
10
- type && type.name.eql?('jpeg')
11
- rescue
12
- false
13
- end
13
+ type, = GdkPixbuf::Pixbuf.get_file_info(filename)
14
14
 
15
- def self.from_string(string, loader: nil, chunk_size: 4096)
16
- loader ||= GdkPixbuf::PixbufLoader.new
17
- ((string.bytesize + chunk_size - 1) / chunk_size).times do |i|
18
- loader.write(string.byteslice(i * chunk_size, chunk_size))
15
+ type && type.name.eql?('jpeg')
16
+ rescue StandardError
17
+ false
19
18
  end
20
- loader.close
21
- loader.pixbuf
22
- end
23
19
 
24
- def self.default_icc_path(path)
25
- "#{path}.icc.jpg"
26
- end
20
+ # TODO: this doesn't use lcms
21
+ def self.from_string(string, loader: nil, chunk_size: 4096)
22
+ loader ||= GdkPixbuf::PixbufLoader.new
23
+ ((string.bytesize + chunk_size - 1) / chunk_size).times do |i|
24
+ loader.write(string.byteslice(i * chunk_size, chunk_size))
25
+ end
26
+ loader.close
27
+ loader.pixbuf
28
+ end
27
29
 
28
- def initialize(*args)
29
- @local_options = args.last.is_a?(Hash) && args.pop || {}
30
+ def self.default_icc_path(path)
31
+ "#{path}.icc.jpg"
32
+ end
30
33
 
31
- if args[0].is_a?(String)
32
- @file = args[0]
34
+ def initialize(file, local_options, scale_to = nil)
35
+ @local_options = local_options
36
+ @file = file
33
37
 
34
38
  if suitable_for_jpegicc?
35
39
  icc_file = icc_cache_path
36
-
37
- args[0] = icc_file if valid_jpeg?(icc_file) || system("jpgicc", "-q97", @file, icc_file)
40
+ valid_jpeg?(icc_file) || system('jpgicc', '-q97', @file, icc_file)
41
+ file = icc_file if valid_jpeg?(icc_file)
38
42
  end
39
- end
40
-
41
- # TODO: This is to fix some deprecation warnings. This needs refactoring.
42
- # All can be implemented without having to hack on the PixBuff gem.
43
- case args.size
44
- when 1
45
- super(file: args.first)
46
- when 3
47
- super(path: args[0], width: args[1], height: args[2])
48
- else
49
- super(*args)
50
- end
51
- rescue GdkPixbuf::PixbufError::CorruptImage => e
52
- if args[0].is_a?(String) && defined? Tempfile
53
- temp = Tempfile.new
54
- pixbuf = self.class.from_string(File.read(args[0]))
55
- pixbuf.save(temp.path, 'jpeg')
56
- args[0] = temp.path
57
43
 
58
- if args.size == 1
59
- super file: args.first
44
+ if scale_to
45
+ super(file: file, width: scale_to, height: scale_to)
60
46
  else
61
- super(*args)
47
+ super(file: file)
62
48
  end
63
-
64
- temp.close
65
- temp.unlink
66
- else
67
- throw e
68
49
  end
69
- end
70
50
 
51
+ private
71
52
 
72
- protected
73
- def suitable_for_jpegicc?
74
- type, _, _ = GdkPixbuf::Pixbuf.get_file_info(@file)
75
-
76
- type && type.name.eql?('jpeg')
77
- end
53
+ def suitable_for_jpegicc?
54
+ valid_jpeg?(@file)
55
+ end
78
56
 
79
- def icc_cache_path
80
- @local_options['path.icc'] || Morandi::ProfiledPixbuf.default_icc_path(@file)
57
+ def icc_cache_path
58
+ @local_options['path.icc'] || Morandi::ProfiledPixbuf.default_icc_path(@file)
59
+ end
81
60
  end
82
61
  end
@@ -1,4 +1,4 @@
1
- require 'redeye'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Morandi
4
4
  module RedEye
@@ -6,57 +6,45 @@ module Morandi
6
6
  # The reason for its existence is to prevent the situations when the bigger red area causes an excessive correction
7
7
  # e.g. continuous red eyeglasses frame or sunburnt person's skin around eyes forming an area
8
8
  RED_AREA_DENSITY_THRESHOLD = 0.3
9
- end
10
- end
11
9
 
12
- module Morandi
13
- module RedEye
14
- module TapRedEye
15
- module_function
16
- def tap_on(pb, x, y)
17
- n = ([pb.height,pb.width].max / 10)
18
- x1 = [x - n, 0].max
19
- x2 = [x + n, pb.width].min
20
- y1 = [y - n, 0].max
21
- y2 = [y + n, pb.height].min
22
- return pb unless (x1 >= 0) && (x2 > x1) && (y1 >= 0) && (y2 > y1)
23
- redeye = ::RedEye.new(pb, x1, y1, x2, y2)
24
-
25
- sensitivity = 2
26
- blobs = redeye.identify_blobs(sensitivity).reject { |region|
27
- region.noPixels < 4 || !region.squareish?(0.5, RED_AREA_DENSITY_THRESHOLD)
28
- }.sort_by { |region|
29
- region.area_min_x = x1
30
- region.area_min_y = y1
31
-
32
- # Higher is better
33
- score = (region.noPixels) / (region.distance_from(x, y) ** 2)
34
- }
35
-
36
- blob = blobs.last
37
- redeye.correct_blob(blob.id) if blob
38
- pb = redeye.pixbuf
39
- end
40
- end
41
- end
42
- end
10
+ # RedEye finder that looks for "eye" closest to a point
11
+ module TapRedEye
12
+ module_function
43
13
 
44
- class ::RedEye::Region
45
- attr_accessor :area_min_x
46
- attr_accessor :area_min_y
47
- def centre
48
- [@area_min_x.to_i + ((maxX + minX) >> 1),
49
- @area_min_y.to_i + ((maxY + minY) >> 1)]
50
- end
14
+ def tap_on(pixbuf, x_coord, y_coord)
15
+ n = ([pixbuf.height, pixbuf.width].max / 10)
16
+ x1 = [x_coord - n, 0].max
17
+ x2 = [x_coord + n, pixbuf.width].min
18
+ y1 = [y_coord - n, 0].max
19
+ y2 = [y_coord + n, pixbuf.height].min
20
+
21
+ return pixbuf unless (x1 >= 0) && (x2 > x1) && (y1 >= 0) && (y2 > y1)
51
22
 
52
- # Pythagorean
53
- def distance_from(x,y)
54
- cx,cy = centre()
23
+ red_eye = MorandiNative::RedEye.new(pixbuf, x1, y1, x2, y2)
55
24
 
56
- dx = cx - x
57
- dy = cy - y
25
+ sensitivity = 2
26
+ blobs = red_eye.identify_blobs(sensitivity).reject do |region|
27
+ region.noPixels < 4 || !region.squareish?(0.5, RED_AREA_DENSITY_THRESHOLD)
28
+ end
58
29
 
59
- Math.sqrt( (dx * dx) + (dy * dy) )
30
+ sorted_blobs = blobs.sort_by do |region|
31
+ region.area_min_x = x1
32
+ region.area_min_y = y1
33
+ end
34
+
35
+ blob = sorted_blobs.last
36
+ red_eye.correct_blob(blob.id) if blob
37
+ red_eye.pixbuf
38
+ end
39
+ end
60
40
  end
61
41
  end
62
42
 
43
+ module MorandiNative
44
+ class RedEye
45
+ # Represents an area with a suspected red eye
46
+ class Region
47
+ attr_accessor :area_min_x, :area_min_y
48
+ end
49
+ end
50
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Morandi
2
- VERSION = '0.12.0'.freeze
4
+ VERSION = '0.99.03'
3
5
  end