capybara-screenshot-diff 1.7.0 → 1.8.0

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