morandi 0.12.0 → 0.99.03

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