capybara-screenshot-diff 1.7.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +29 -0
  3. data/capybara-screenshot-diff.gemspec +7 -4
  4. data/gems.rb +8 -2
  5. data/lib/capybara/screenshot/diff/browser_helpers.rb +29 -28
  6. data/lib/capybara/screenshot/diff/cucumber.rb +11 -0
  7. data/lib/capybara/screenshot/diff/difference.rb +63 -0
  8. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +42 -0
  9. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +188 -260
  10. data/lib/capybara/screenshot/diff/drivers/utils.rb +16 -0
  11. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +53 -103
  12. data/lib/capybara/screenshot/diff/drivers.rb +16 -0
  13. data/lib/capybara/screenshot/diff/image_compare.rb +125 -154
  14. data/lib/capybara/screenshot/diff/os.rb +1 -1
  15. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +128 -0
  16. data/lib/capybara/screenshot/diff/screenshoter.rb +137 -0
  17. data/lib/capybara/screenshot/diff/stabilization.rb +0 -184
  18. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +106 -0
  19. data/lib/capybara/screenshot/diff/test_methods.rb +51 -90
  20. data/lib/capybara/screenshot/diff/vcs.rb +44 -22
  21. data/lib/capybara/screenshot/diff/version.rb +1 -1
  22. data/lib/capybara/screenshot/diff.rb +13 -17
  23. data/sig/capybara/screenshot/diff/diff.rbs +28 -0
  24. data/sig/capybara/screenshot/diff/difference.rbs +33 -0
  25. data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +63 -0
  26. data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +36 -0
  27. data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +89 -0
  28. data/sig/capybara/screenshot/diff/drivers/utils.rbs +13 -0
  29. data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +25 -0
  30. data/sig/capybara/screenshot/diff/image_compare.rbs +93 -0
  31. data/sig/capybara/screenshot/diff/os.rbs +11 -0
  32. data/sig/capybara/screenshot/diff/region.rbs +43 -0
  33. data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +60 -0
  34. data/sig/capybara/screenshot/diff/screenshoter.rbs +48 -0
  35. data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +29 -0
  36. data/sig/capybara/screenshot/diff/test_methods.rbs +39 -0
  37. data/sig/capybara/screenshot/diff/vcs.rbs +17 -0
  38. metadata +36 -27
  39. data/.gitattributes +0 -4
  40. data/.github/dependabot.yml +0 -8
  41. data/.github/workflows/lint.yml +0 -25
  42. data/.github/workflows/test.yml +0 -138
  43. data/.gitignore +0 -14
  44. data/.standard.yml +0 -12
  45. data/CONTRIBUTING.md +0 -24
  46. data/Dockerfile +0 -59
  47. data/README.md +0 -567
  48. data/bin/bundle +0 -114
  49. data/bin/console +0 -15
  50. data/bin/install-vips +0 -11
  51. data/bin/rake +0 -27
  52. data/bin/setup +0 -8
  53. data/bin/standardrb +0 -29
  54. data/gemfiles/rails60_gems.rb +0 -8
  55. data/gemfiles/rails61_gems.rb +0 -7
  56. data/gemfiles/rails70_gems.rb +0 -7
  57. data/tmp/.keep +0 -0
@@ -2,38 +2,17 @@
2
2
 
3
3
  require "chunky_png"
4
4
 
5
+ require "capybara/screenshot/diff/drivers/base_driver"
6
+
5
7
  module Capybara
6
8
  module Screenshot
7
9
  module Diff
8
10
  # Compare two images and determine if they are equal, different, or within some comparison
9
11
  # range considering color values and difference area size.
10
12
  module Drivers
11
- class ChunkyPNGDriver
13
+ class ChunkyPNGDriver < BaseDriver
12
14
  include ChunkyPNG::Color
13
15
 
14
- attr_reader :new_file_name, :old_file_name
15
- attr_accessor :skip_area, :color_distance_limit, :shift_distance_limit
16
-
17
- def initialize(new_file_name, old_file_name = nil, options = {})
18
- options = old_file_name if old_file_name.is_a?(Hash)
19
-
20
- @new_file_name = new_file_name
21
- @old_file_name = old_file_name || "#{new_file_name}#{ImageCompare::TMP_FILE_SUFFIX}"
22
-
23
- @color_distance_limit = options[:color_distance_limit]
24
- @shift_distance_limit = options[:shift_distance_limit]
25
- @skip_area = options[:skip_area]
26
-
27
- reset
28
- end
29
-
30
- # Resets the calculated data about the comparison with regard to the "new_image".
31
- # Data about the original image is kept.
32
- def reset
33
- @max_color_distance = @color_distance_limit ? 0 : nil
34
- @max_shift_distance = @shift_distance_limit ? 0 : nil
35
- end
36
-
37
16
  def load_images(old_file_name, new_file_name)
38
17
  old_bytes, new_bytes = load_image_files(old_file_name, new_file_name)
39
18
 
@@ -48,65 +27,8 @@ module Capybara
48
27
  image
49
28
  end
50
29
 
51
- def difference_level(_diff_mask, old_img, region)
52
- size(region).to_f / image_area_size(old_img)
53
- end
54
-
55
- def image_area_size(old_img)
56
- width_for(old_img) * height_for(old_img)
57
- end
58
-
59
- def shift_distance_equal?
60
- # Stub
61
- false
62
- end
63
-
64
- def shift_distance_different?
65
- # Stub
66
- true
67
- end
68
-
69
- def find_difference_region(new_image, old_image, color_distance_limit, shift_distance_limit, area_size_limit, fast_fail: false)
70
- return nil, nil if new_image.pixels == old_image.pixels
71
-
72
- if fast_fail && !(color_distance_limit || shift_distance_limit || area_size_limit)
73
- return build_region_for_whole_image(new_image), nil
74
- end
75
-
76
- region = find_top(old_image, new_image)
77
- region = if region.nil? || region[1].nil?
78
- nil
79
- else
80
- find_diff_rectangle(old_image, new_image, region)
81
- end
82
-
83
- [region, nil]
84
- end
85
-
86
- def height_for(image)
87
- image.height
88
- end
89
-
90
- def width_for(image)
91
- image.width
92
- end
93
-
94
- def max_color_distance
95
- calculate_metrics unless @max_color_distance
96
- @max_color_distance
97
- end
98
-
99
- def max_shift_distance
100
- calculate_metrics unless @max_shift_distance || !@shift_distance_limit
101
- @max_shift_distance
102
- end
103
-
104
- def adds_error_details_to(log)
105
- max_color_distance = self.max_color_distance.ceil(1)
106
- max_shift_distance = self.max_shift_distance
107
-
108
- log[:max_color_distance] = max_color_distance
109
- log.merge!(max_shift_distance: max_shift_distance) if max_shift_distance
30
+ def find_difference_region(comparison)
31
+ DifferenceRegionFinder.new(comparison, self).perform
110
32
  end
111
33
 
112
34
  def crop(region, i)
@@ -114,46 +36,7 @@ module Capybara
114
36
  end
115
37
 
116
38
  def from_file(filename)
117
- ChunkyPNG::Image.from_file(filename)
118
- end
119
-
120
- # private
121
-
122
- def calculate_metrics
123
- old_file, new_file = load_image_files(@old_file_name, @new_file_name)
124
-
125
- if old_file == new_file
126
- @max_color_distance = 0
127
- @max_shift_distance = 0
128
- return
129
- end
130
-
131
- old_image, new_image = _load_images(old_file, new_file)
132
- calculate_max_color_distance(new_image, old_image)
133
- calculate_max_shift_limit(new_image, old_image) if @shift_distance_limit
134
- end
135
-
136
- def calculate_max_color_distance(new_image, old_image)
137
- pixel_pairs = old_image.pixels.zip(new_image.pixels)
138
- @max_color_distance = pixel_pairs.inject(0) { |max, (p1, p2)|
139
- next max unless p1 && p2
140
-
141
- d = ChunkyPNG::Color.euclidean_distance_rgba(p1, p2)
142
- [max, d].max
143
- }
144
- end
145
-
146
- def calculate_max_shift_limit(new_img, old_img)
147
- (0...new_img.width).each do |x|
148
- (0...new_img.height).each do |y|
149
- shift_distance =
150
- shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
151
- if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
152
- @max_shift_distance = shift_distance
153
- return if @max_shift_distance == Float::INFINITY # rubocop: disable Lint/NonLocalExitFromIterator
154
- end
155
- end
156
- end
39
+ ChunkyPNG::Image.from_file(filename.to_s)
157
40
  end
158
41
 
159
42
  def save_image_to(image, filename)
@@ -165,203 +48,248 @@ module Capybara
165
48
  end
166
49
 
167
50
  def load_image_files(old_file_name, file_name)
168
- old_file = File.binread(old_file_name)
169
- new_file = File.binread(file_name)
170
- [old_file, new_file]
171
- end
172
-
173
- def dimension_changed?(old_image, new_image)
174
- return unless old_image.dimension != new_image.dimension
175
-
176
- change_msg = [old_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
177
- warn "Image size has changed for #{@new_file_name}: #{change_msg}"
178
- true
51
+ [File.binread(old_file_name), File.binread(file_name)]
179
52
  end
180
53
 
181
- def draw_rectangles(images, region, (r, g, b))
54
+ def draw_rectangles(images, region, (r, g, b), offset: 0)
182
55
  border_color = ChunkyPNG::Color.rgb(r, g, b)
183
56
  border_shadow = ChunkyPNG::Color.rgba(r, g, b, 100)
184
57
 
185
58
  images.map do |image|
186
59
  new_img = image.dup
187
- new_img.rect(region.left - 1, region.top - 1, region.right + 1, region.bottom + 1, border_color)
60
+ new_img.rect(region.left - offset, region.top - offset, region.right + offset, region.bottom + offset, border_color)
188
61
  new_img.rect(region.left, region.top, region.right, region.bottom, border_shadow)
189
62
  new_img
190
63
  end
191
64
  end
192
65
 
66
+ def same_pixels?(comparison)
67
+ comparison.new_image == comparison.base_image
68
+ end
69
+
193
70
  private
194
71
 
195
- def build_region_for_whole_image(new_image)
196
- Region.from_edge_coordinates(0, 0, width_for(new_image), height_for(new_image))
72
+ def _load_images(old_file, new_file)
73
+ [ChunkyPNG::Image.from_blob(old_file), ChunkyPNG::Image.from_blob(new_file)]
197
74
  end
198
75
 
199
- def find_diff_rectangle(org_img, new_img, area_coordinates)
200
- left, top, right, bottom = find_left_right_and_top(org_img, new_img, area_coordinates)
201
- bottom = find_bottom(org_img, new_img, left, right, bottom)
76
+ class DifferenceRegionFinder
77
+ attr_accessor :skip_area, :color_distance_limit, :shift_distance_limit
202
78
 
203
- Region.from_edge_coordinates(left, top, right, bottom)
204
- end
79
+ def initialize(comparison, driver = nil)
80
+ @comparison = comparison
81
+ @driver = driver
205
82
 
206
- def find_top(old_img, new_img)
207
- old_img.height.times do |y|
208
- old_img.width.times do |x|
209
- return [x, y, x, y] unless same_color?(old_img, new_img, x, y)
210
- end
83
+ @color_distance_limit = comparison.options[:color_distance_limit]
84
+ @shift_distance_limit = comparison.options[:shift_distance_limit]
85
+ @skip_area = comparison.options[:skip_area]
211
86
  end
212
- nil
213
- end
214
87
 
215
- def find_left_right_and_top(old_img, new_img, region)
216
- region = region.is_a?(Region) ? region.to_edge_coordinates : region
88
+ def perform
89
+ find_difference_region(@comparison)
90
+ end
217
91
 
218
- left = region[0] || old_img.width - 1
219
- top = region[1]
220
- right = region[2] || 0
221
- bottom = region[3]
92
+ def find_difference_region(comparison)
93
+ new_image, base_image, = comparison.new_image, comparison.base_image
222
94
 
223
- old_img.height.times do |y|
224
- (0...left).find do |x|
225
- next if same_color?(old_img, new_img, x, y)
95
+ meta = {}
96
+ meta[:max_color_distance] = 0
97
+ meta[:max_shift_distance] = 0 if shift_distance_limit
226
98
 
227
- top ||= y
228
- bottom = y
229
- left = x
230
- right = x if x > right
231
- x
99
+ region = find_top(base_image, new_image, cache: meta)
100
+ region = if region.nil? || region[1].nil?
101
+ nil
102
+ else
103
+ find_diff_rectangle(base_image, new_image, region, cache: meta)
232
104
  end
233
- (old_img.width - 1).step(right + 1, -1).find do |x|
234
- unless same_color?(old_img, new_img, x, y)
235
- bottom = y
236
- right = x
105
+
106
+ result = Difference.new(region, meta, comparison)
107
+
108
+ unless result.blank?
109
+ meta[:max_color_distance] = meta[:max_color_distance].ceil(1) if meta[:max_color_distance]
110
+
111
+ if comparison.options[:tolerance]
112
+ meta[:difference_level] = difference_level(nil, base_image, region)
237
113
  end
238
114
  end
115
+
116
+ result
239
117
  end
240
118
 
241
- [left, top, right, bottom]
242
- end
119
+ def difference_level(_diff_mask, base_image, region)
120
+ image_area_size = @driver.image_area_size(base_image)
121
+ return nil if image_area_size.zero?
122
+
123
+ region.size.to_f / image_area_size
124
+ end
243
125
 
244
- def find_bottom(old_img, new_img, left, right, bottom)
245
- if bottom
246
- (old_img.height - 1).step(bottom + 1, -1).find do |y|
247
- (left..right).find do |x|
248
- bottom = y unless same_color?(old_img, new_img, x, y)
126
+ def find_diff_rectangle(org_img, new_img, area_coordinates, cache:)
127
+ left, top, right, bottom = find_left_right_and_top(org_img, new_img, area_coordinates, cache: cache)
128
+ bottom = find_bottom(org_img, new_img, left, right, bottom, cache: cache)
129
+
130
+ Region.from_edge_coordinates(left, top, right, bottom)
131
+ end
132
+
133
+ def find_top(old_img, new_img, cache:)
134
+ old_img.height.times do |y|
135
+ old_img.width.times do |x|
136
+ return [x, y, x, y] unless same_color?(old_img, new_img, x, y, cache: cache)
249
137
  end
250
138
  end
139
+ nil
251
140
  end
252
141
 
253
- bottom
254
- end
142
+ def find_left_right_and_top(old_img, new_img, region, cache:)
143
+ region = region.is_a?(Region) ? region.to_edge_coordinates : region
144
+
145
+ left = region[0] || old_img.width - 1
146
+ top = region[1]
147
+ right = region[2] || 0
148
+ bottom = region[3]
149
+
150
+ old_img.height.times do |y|
151
+ (0...left).find do |x|
152
+ next if same_color?(old_img, new_img, x, y, cache: cache)
255
153
 
256
- def same_color?(old_img, new_img, x, y)
257
- return true if skipped_region?(x, y)
154
+ top ||= y
155
+ bottom = y
156
+ left = x
157
+ right = x if x > right
158
+ x
159
+ end
160
+ (old_img.width - 1).step(right + 1, -1).find do |x|
161
+ unless same_color?(old_img, new_img, x, y, cache: cache)
162
+ bottom = y
163
+ right = x
164
+ end
165
+ end
166
+ end
258
167
 
259
- color_distance =
260
- color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit)
261
- if !@max_color_distance || color_distance > @max_color_distance
262
- @max_color_distance = color_distance
168
+ [left, top, right, bottom]
263
169
  end
264
- color_matches = color_distance == 0 || (@color_distance_limit && @color_distance_limit > 0 &&
265
- color_distance <= @color_distance_limit)
266
- return color_matches if !@shift_distance_limit || @max_shift_distance == Float::INFINITY
267
-
268
- shift_distance = (color_matches && 0) ||
269
- shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
270
- if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
271
- @max_shift_distance = shift_distance
170
+
171
+ def find_bottom(old_img, new_img, left, right, bottom, cache:)
172
+ if bottom
173
+ (old_img.height - 1).step(bottom + 1, -1).find do |y|
174
+ (left..right).find do |x|
175
+ bottom = y unless same_color?(old_img, new_img, x, y, cache: cache)
176
+ end
177
+ end
178
+ end
179
+
180
+ bottom
272
181
  end
273
- color_matches
274
- end
275
182
 
276
- def skipped_region?(x, y)
277
- return false unless @skip_area
183
+ def same_color?(old_img, new_img, x, y, cache:)
184
+ return true if skipped_region?(x, y)
278
185
 
279
- @skip_area.any? { |region| region.cover?(x, y) }
280
- end
186
+ color_distance =
187
+ color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit)
188
+
189
+ if color_distance > cache[:max_color_distance]
190
+ cache[:max_color_distance] = color_distance
191
+ end
192
+
193
+ color_matches = color_distance == 0 ||
194
+ (!!@color_distance_limit && @color_distance_limit > 0 && color_distance <= @color_distance_limit)
281
195
 
282
- def color_distance_at(new_img, old_img, x, y, shift_distance_limit:)
283
- org_color = old_img[x, y]
284
- if shift_distance_limit
285
- start_x = [0, x - shift_distance_limit].max
286
- end_x = [x + shift_distance_limit, new_img.width - 1].min
287
- xs = (start_x..end_x).to_a
288
- start_y = [0, y - shift_distance_limit].max
289
- end_y = [y + shift_distance_limit, new_img.height - 1].min
290
- ys = (start_y..end_y).to_a
291
- new_pixels = xs.product(ys)
292
- distances = new_pixels.map { |dx, dy|
293
- new_color = new_img[dx, dy]
294
- ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
295
- }
296
- distances.min
297
- else
298
- ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
196
+ return color_matches if !@shift_distance_limit || cache[:max_shift_distance] == Float::INFINITY
197
+
198
+ shift_distance = (color_matches && 0) ||
199
+ shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
200
+ if shift_distance && (cache[:max_shift_distance].nil? || shift_distance > cache[:max_shift_distance])
201
+ cache[:max_shift_distance] = shift_distance
202
+ end
203
+
204
+ color_matches
299
205
  end
300
- end
301
206
 
302
- def shift_distance_at(new_img, old_img, x, y, color_distance_limit:)
303
- org_color = old_img[x, y]
304
- shift_distance = 0
305
- loop do
306
- bounds_breached = 0
307
- top_row = y - shift_distance
308
- if top_row >= 0 # top
309
- ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
310
- if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
311
- return shift_distance
312
- end
207
+ def skipped_region?(x, y)
208
+ return false unless @skip_area
209
+
210
+ @skip_area.any? { |region| region.cover?(x, y) }
211
+ end
212
+
213
+ def color_distance_at(new_img, old_img, x, y, shift_distance_limit:)
214
+ org_color = old_img[x, y]
215
+ if shift_distance_limit
216
+ start_x = [0, x - shift_distance_limit].max
217
+ end_x = [x + shift_distance_limit, new_img.width - 1].min
218
+ xs = (start_x..end_x).to_a
219
+ start_y = [0, y - shift_distance_limit].max
220
+ end_y = [y + shift_distance_limit, new_img.height - 1].min
221
+ ys = (start_y..end_y).to_a
222
+ new_pixels = xs.product(ys)
223
+
224
+ distances = new_pixels.map do |dx, dy|
225
+ ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[dx, dy])
313
226
  end
227
+ distances.min
314
228
  else
315
- bounds_breached += 1
229
+ ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
316
230
  end
317
- if shift_distance > 0
318
- if (x - shift_distance) >= 0 # left
319
- ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
320
- .each do |dy|
321
- if color_matches(new_img, org_color, x - shift_distance, dy, color_distance_limit)
231
+ end
232
+
233
+ def shift_distance_at(new_img, old_img, x, y, color_distance_limit:)
234
+ org_color = old_img[x, y]
235
+ shift_distance = 0
236
+ loop do
237
+ bounds_breached = 0
238
+ top_row = y - shift_distance
239
+ if top_row >= 0 # top
240
+ ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
241
+ if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
322
242
  return shift_distance
323
243
  end
324
244
  end
325
245
  else
326
246
  bounds_breached += 1
327
247
  end
328
- if (y + shift_distance) < new_img.height # bottom
329
- ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
330
- if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
331
- return shift_distance
248
+ if shift_distance > 0
249
+ if (x - shift_distance) >= 0 # left
250
+ ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
251
+ .each do |dy|
252
+ if color_matches(new_img, org_color, x - shift_distance, dy, color_distance_limit)
253
+ return shift_distance
254
+ end
332
255
  end
256
+ else
257
+ bounds_breached += 1
333
258
  end
334
- else
335
- bounds_breached += 1
336
- end
337
- if (x + shift_distance) < new_img.width # right
338
- ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
339
- .each do |dy|
340
- if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
341
- return shift_distance
259
+ if (y + shift_distance) < new_img.height # bottom
260
+ ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
261
+ if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
262
+ return shift_distance
263
+ end
342
264
  end
265
+ else
266
+ bounds_breached += 1
267
+ end
268
+ if (x + shift_distance) < new_img.width # right
269
+ ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
270
+ .each do |dy|
271
+ if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
272
+ return shift_distance
273
+ end
274
+ end
275
+ else
276
+ bounds_breached += 1
343
277
  end
344
- else
345
- bounds_breached += 1
346
278
  end
347
- end
348
- break if bounds_breached == 4
279
+ break if bounds_breached == 4
349
280
 
350
- shift_distance += 1
281
+ shift_distance += 1
282
+ end
283
+ Float::INFINITY
351
284
  end
352
- Float::INFINITY
353
- end
354
285
 
355
- def color_matches(new_img, org_color, x, y, color_distance_limit)
356
- new_color = new_img[x, y]
357
- return new_color == org_color unless color_distance_limit
358
-
359
- color_distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
360
- color_distance <= color_distance_limit
361
- end
286
+ def color_matches(new_img, org_color, x, y, color_distance_limit)
287
+ new_color = new_img[x, y]
288
+ return new_color == org_color unless color_distance_limit
362
289
 
363
- def _load_images(old_file, new_file)
364
- [ChunkyPNG::Image.from_blob(old_file), ChunkyPNG::Image.from_blob(new_file)]
290
+ color_distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
291
+ color_distance <= color_distance_limit
292
+ end
365
293
  end
366
294
  end
367
295
  end
@@ -19,6 +19,22 @@ module Capybara
19
19
  result
20
20
  end
21
21
 
22
+ def self.find_driver_class_for(driver)
23
+ driver = AVAILABLE_DRIVERS.first if driver == :auto
24
+
25
+ LOADED_DRIVERS[driver] ||=
26
+ case driver
27
+ when :chunky_png
28
+ require "capybara/screenshot/diff/drivers/chunky_png_driver"
29
+ Drivers::ChunkyPNGDriver
30
+ when :vips
31
+ require "capybara/screenshot/diff/drivers/vips_driver"
32
+ Drivers::VipsDriver
33
+ else
34
+ fail "Wrong adapter #{driver.inspect}. Available adapters: #{AVAILABLE_DRIVERS.inspect}"
35
+ end
36
+ end
37
+
22
38
  def self.detect_test_framework_assert
23
39
  require "minitest"
24
40
  ::Minitest::Assertion