capybara-screenshot-diff 1.15.1 → 2.0.0.alpha1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/docs/thread_safety.md +15 -0
- data/lib/capybara/screenshot/diff/annotation_service.rb +5 -77
- data/lib/capybara/screenshot/diff/area_calculator.rb +5 -54
- data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -119
- data/lib/capybara/screenshot/diff/config_legacy.rb +113 -0
- data/lib/capybara/screenshot/diff/cucumber.rb +1 -1
- data/lib/capybara/screenshot/diff/difference.rb +6 -102
- data/lib/capybara/screenshot/diff/drivers/base_driver.rb +7 -39
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +5 -299
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +5 -169
- data/lib/capybara/screenshot/diff/drivers.rb +7 -14
- data/lib/capybara/screenshot/diff/image_compare.rb +10 -228
- data/lib/capybara/screenshot/diff/image_preprocessor.rb +5 -70
- data/lib/capybara/screenshot/diff/os.rb +8 -13
- data/lib/capybara/screenshot/diff/reporters/default.rb +5 -3
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +5 -129
- data/lib/capybara/screenshot/diff/screenshoter.rb +5 -127
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +5 -104
- data/lib/capybara/screenshot/diff/utils.rb +5 -40
- data/lib/capybara/screenshot/diff/vcs.rb +5 -37
- data/lib/capybara/screenshot/diff/version.rb +7 -1
- data/lib/capybara_screenshot_diff/attempts_reporter.rb +5 -47
- data/lib/capybara_screenshot_diff/cucumber.rb +5 -17
- data/lib/capybara_screenshot_diff/dsl.rb +7 -129
- data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +6 -30
- data/lib/capybara_screenshot_diff/minitest.rb +6 -56
- data/lib/capybara_screenshot_diff/reporters/html.rb +5 -135
- data/lib/capybara_screenshot_diff/rspec.rb +5 -63
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +39 -157
- data/lib/capybara_screenshot_diff/screenshot_namer.rb +5 -79
- data/lib/capybara_screenshot_diff/snap.rb +5 -64
- data/lib/capybara_screenshot_diff/snap_manager.rb +5 -82
- data/lib/capybara_screenshot_diff/static.rb +3 -5
- data/lib/capybara_screenshot_diff.rb +29 -100
- data/lib/snap_diff/annotation_service.rb +84 -0
- data/lib/snap_diff/area_calculator.rb +56 -0
- data/lib/snap_diff/attempts_reporter.rb +51 -0
- data/lib/snap_diff/browser_helpers.rb +121 -0
- data/lib/snap_diff/capture/viewport.rb +40 -0
- data/lib/snap_diff/comparison.rb +238 -0
- data/lib/snap_diff/comparison_result.rb +104 -0
- data/lib/snap_diff/config.rb +6 -2
- data/lib/snap_diff/deprecation.rb +84 -0
- data/lib/snap_diff/driver.rb +37 -0
- data/lib/snap_diff/drivers/chunky_png_driver.rb +298 -0
- data/lib/snap_diff/drivers/vips_driver.rb +171 -0
- data/lib/snap_diff/drivers.rb +14 -0
- data/lib/snap_diff/dsl.rb +143 -0
- data/lib/snap_diff/error_with_filtered_backtrace.rb +32 -0
- data/lib/snap_diff/image_preprocessor.rb +68 -0
- data/lib/snap_diff/integrations/cucumber.rb +22 -0
- data/lib/snap_diff/integrations/minitest.rb +70 -0
- data/lib/snap_diff/integrations/rspec.rb +68 -0
- data/lib/snap_diff/legacy_shims.rb +99 -0
- data/lib/snap_diff/os.rb +17 -0
- data/lib/snap_diff/reporters/html.rb +143 -0
- data/lib/snap_diff/reporting.rb +53 -0
- data/lib/snap_diff/screenshot_assertion.rb +143 -0
- data/lib/snap_diff/screenshot_matcher.rb +113 -0
- data/lib/snap_diff/screenshot_namer.rb +81 -0
- data/lib/snap_diff/screenshoter.rb +129 -0
- data/lib/snap_diff/snap.rb +66 -0
- data/lib/snap_diff/snap_manager.rb +125 -0
- data/lib/snap_diff/stable_screenshoter.rb +103 -0
- data/lib/snap_diff/static.rb +11 -0
- data/lib/snap_diff/utils.rb +38 -0
- data/lib/snap_diff/vcs.rb +35 -0
- data/lib/snap_diff/version.rb +5 -0
- data/lib/snap_diff.rb +22 -6
- metadata +36 -2
- /data/lib/{capybara_screenshot_diff → snap_diff}/reporters/templates/report.html.erb +0 -0
|
@@ -1,301 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
require "capybara/screenshot/diff/drivers/base_driver"
|
|
11
|
-
|
|
12
|
-
module Capybara
|
|
13
|
-
module Screenshot
|
|
14
|
-
module Diff
|
|
15
|
-
# Compare two images and determine if they are equal, different, or within some comparison
|
|
16
|
-
# range considering color values and difference area size.
|
|
17
|
-
module Drivers
|
|
18
|
-
class ChunkyPNGDriver < BaseDriver
|
|
19
|
-
include ChunkyPNG::Color
|
|
20
|
-
|
|
21
|
-
def load_images(old_file_name, new_file_name)
|
|
22
|
-
old_bytes, new_bytes = load_image_files(old_file_name, new_file_name)
|
|
23
|
-
|
|
24
|
-
_load_images(old_bytes, new_bytes)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def add_black_box(image, _region)
|
|
28
|
-
image
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def find_difference_region(comparison)
|
|
32
|
-
DifferenceRegionFinder.new(comparison, self).perform
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def crop(region, i)
|
|
36
|
-
i.crop(*region.to_top_left_corner_coordinates)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def from_file(filename_or_path)
|
|
40
|
-
ChunkyPNG::Image.from_file(filename_or_path.to_s)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def save_image_to(image, filename)
|
|
44
|
-
image.save(filename, :fast_rgba)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def resize_image_to(image, new_width, new_height)
|
|
48
|
-
image.resample_bilinear(new_width, new_height)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def load_image_files(old_file_name, file_name)
|
|
52
|
-
[old_file_name.binread, file_name.binread]
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def draw_rectangles(images, region, (r, g, b), offset: 0)
|
|
56
|
-
border_color = ChunkyPNG::Color.rgb(r, g, b)
|
|
57
|
-
border_shadow = ChunkyPNG::Color.rgba(r, g, b, 100)
|
|
58
|
-
|
|
59
|
-
images.map do |image|
|
|
60
|
-
new_img = image.dup
|
|
61
|
-
new_img.rect(region.left - offset, region.top - offset, region.right + offset, region.bottom + offset, border_color)
|
|
62
|
-
new_img.rect(region.left, region.top, region.right, region.bottom, border_shadow)
|
|
63
|
-
new_img
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def same_pixels?(comparison)
|
|
68
|
-
comparison.new_image == comparison.base_image
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def _load_images(old_file, new_file)
|
|
74
|
-
[ChunkyPNG::Image.from_blob(old_file), ChunkyPNG::Image.from_blob(new_file)]
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
class DifferenceRegionFinder
|
|
78
|
-
attr_accessor :skip_area, :color_distance_limit, :shift_distance_limit
|
|
79
|
-
|
|
80
|
-
def initialize(comparison, driver = nil)
|
|
81
|
-
@comparison = comparison
|
|
82
|
-
@driver = driver
|
|
83
|
-
|
|
84
|
-
@color_distance_limit = comparison.options[:color_distance_limit]
|
|
85
|
-
@shift_distance_limit = comparison.options[:shift_distance_limit]
|
|
86
|
-
@skip_area = comparison.options[:skip_area]
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def perform
|
|
90
|
-
find_difference_region(@comparison)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def find_difference_region(comparison)
|
|
94
|
-
new_image, base_image, = comparison.new_image, comparison.base_image
|
|
95
|
-
|
|
96
|
-
meta = {}
|
|
97
|
-
meta[:max_color_distance] = 0
|
|
98
|
-
meta[:max_shift_distance] = 0 if shift_distance_limit
|
|
99
|
-
|
|
100
|
-
region = find_top(base_image, new_image, cache: meta)
|
|
101
|
-
region = if region.nil? || region[1].nil?
|
|
102
|
-
nil
|
|
103
|
-
else
|
|
104
|
-
find_diff_rectangle(base_image, new_image, region, cache: meta)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
result = Difference.new(region, meta, comparison)
|
|
108
|
-
|
|
109
|
-
unless result.blank?
|
|
110
|
-
meta[:max_color_distance] = meta[:max_color_distance].ceil(1) if meta[:max_color_distance]
|
|
111
|
-
|
|
112
|
-
if comparison.options[:tolerance]
|
|
113
|
-
meta[:difference_level] = difference_level(nil, base_image, region)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
result
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def difference_level(_diff_mask, base_image, region)
|
|
121
|
-
image_area_size = @driver.image_area_size(base_image)
|
|
122
|
-
return nil if image_area_size.zero?
|
|
123
|
-
|
|
124
|
-
region.size.to_f / image_area_size
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def find_diff_rectangle(org_img, new_img, area_coordinates, cache:)
|
|
128
|
-
left, top, right, bottom = find_left_right_and_top(org_img, new_img, area_coordinates, cache: cache)
|
|
129
|
-
bottom = find_bottom(org_img, new_img, left, right, bottom, cache: cache)
|
|
130
|
-
|
|
131
|
-
Region.from_edge_coordinates(left, top, right, bottom)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def find_top(old_img, new_img, cache:)
|
|
135
|
-
old_img.height.times do |y|
|
|
136
|
-
old_img.width.times do |x|
|
|
137
|
-
return [x, y, x, y] unless same_color?(old_img, new_img, x, y, cache: cache)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
nil
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def find_left_right_and_top(old_img, new_img, region, cache:)
|
|
144
|
-
region = region.is_a?(Region) ? region.to_edge_coordinates : region
|
|
145
|
-
|
|
146
|
-
left = region[0] || old_img.width - 1
|
|
147
|
-
top = region[1]
|
|
148
|
-
right = region[2] || 0
|
|
149
|
-
bottom = region[3]
|
|
150
|
-
|
|
151
|
-
old_img.height.times do |y|
|
|
152
|
-
(0...left).find do |x|
|
|
153
|
-
next if same_color?(old_img, new_img, x, y, cache: cache)
|
|
154
|
-
|
|
155
|
-
top ||= y
|
|
156
|
-
bottom = y
|
|
157
|
-
left = x
|
|
158
|
-
right = x if x > right
|
|
159
|
-
x
|
|
160
|
-
end
|
|
161
|
-
(old_img.width - 1).step(right + 1, -1).find do |x|
|
|
162
|
-
unless same_color?(old_img, new_img, x, y, cache: cache)
|
|
163
|
-
bottom = y
|
|
164
|
-
right = x
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
[left, top, right, bottom]
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def find_bottom(old_img, new_img, left, right, bottom, cache:)
|
|
173
|
-
if bottom
|
|
174
|
-
(old_img.height - 1).step(bottom + 1, -1).find do |y|
|
|
175
|
-
(left..right).find do |x|
|
|
176
|
-
bottom = y unless same_color?(old_img, new_img, x, y, cache: cache)
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
bottom
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def same_color?(old_img, new_img, x, y, cache:)
|
|
185
|
-
return true if skipped_region?(x, y)
|
|
186
|
-
|
|
187
|
-
color_distance =
|
|
188
|
-
color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit)
|
|
189
|
-
|
|
190
|
-
if color_distance > cache[:max_color_distance]
|
|
191
|
-
cache[:max_color_distance] = color_distance
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
color_matches = color_distance == 0 ||
|
|
195
|
-
(!!@color_distance_limit && @color_distance_limit > 0 && color_distance <= @color_distance_limit)
|
|
196
|
-
|
|
197
|
-
return color_matches if !@shift_distance_limit || cache[:max_shift_distance] == Float::INFINITY
|
|
198
|
-
|
|
199
|
-
shift_distance = (color_matches && 0) ||
|
|
200
|
-
shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
|
|
201
|
-
if shift_distance && (cache[:max_shift_distance].nil? || shift_distance > cache[:max_shift_distance])
|
|
202
|
-
cache[:max_shift_distance] = shift_distance
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
color_matches
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def skipped_region?(x, y)
|
|
209
|
-
return false unless @skip_area
|
|
210
|
-
|
|
211
|
-
@skip_area.any? { |region| region.cover?(x, y) }
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def color_distance_at(new_img, old_img, x, y, shift_distance_limit:)
|
|
215
|
-
org_color = old_img[x, y]
|
|
216
|
-
unless shift_distance_limit
|
|
217
|
-
return ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
start_x = [0, x - shift_distance_limit].max
|
|
221
|
-
end_x = [x + shift_distance_limit, new_img.width - 1].min
|
|
222
|
-
start_y = [0, y - shift_distance_limit].max
|
|
223
|
-
end_y = [y + shift_distance_limit, new_img.height - 1].min
|
|
224
|
-
|
|
225
|
-
min_distance = Float::INFINITY
|
|
226
|
-
(start_y..end_y).each do |dy|
|
|
227
|
-
(start_x..end_x).each do |dx|
|
|
228
|
-
distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[dx, dy])
|
|
229
|
-
return 0 if distance == 0
|
|
230
|
-
min_distance = distance if distance < min_distance
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
min_distance
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def shift_distance_at(new_img, old_img, x, y, color_distance_limit:)
|
|
237
|
-
org_color = old_img[x, y]
|
|
238
|
-
shift_distance = 0
|
|
239
|
-
loop do
|
|
240
|
-
bounds_breached = 0
|
|
241
|
-
top_row = y - shift_distance
|
|
242
|
-
if top_row >= 0 # top
|
|
243
|
-
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
|
244
|
-
if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
|
|
245
|
-
return shift_distance
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
else
|
|
249
|
-
bounds_breached += 1
|
|
250
|
-
end
|
|
251
|
-
if shift_distance > 0
|
|
252
|
-
if (x - shift_distance) >= 0 # left
|
|
253
|
-
([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
|
|
254
|
-
.each do |dy|
|
|
255
|
-
if color_matches(new_img, org_color, x - shift_distance, dy, color_distance_limit)
|
|
256
|
-
return shift_distance
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
else
|
|
260
|
-
bounds_breached += 1
|
|
261
|
-
end
|
|
262
|
-
if (y + shift_distance) < new_img.height # bottom
|
|
263
|
-
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
|
264
|
-
if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
|
|
265
|
-
return shift_distance
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
else
|
|
269
|
-
bounds_breached += 1
|
|
270
|
-
end
|
|
271
|
-
if (x + shift_distance) < new_img.width # right
|
|
272
|
-
([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
|
|
273
|
-
.each do |dy|
|
|
274
|
-
if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
|
|
275
|
-
return shift_distance
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
else
|
|
279
|
-
bounds_breached += 1
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
break if bounds_breached == 4
|
|
283
|
-
|
|
284
|
-
shift_distance += 1
|
|
285
|
-
end
|
|
286
|
-
Float::INFINITY
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def color_matches(new_img, org_color, x, y, color_distance_limit)
|
|
290
|
-
new_color = new_img[x, y]
|
|
291
|
-
return new_color == org_color unless color_distance_limit
|
|
292
|
-
|
|
293
|
-
color_distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
|
|
294
|
-
color_distance <= color_distance_limit
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
end
|
|
3
|
+
# Forwarder (ADR-004 v2 step 4): ChunkyPNGDriver lives in SnapDiff::Drivers
|
|
4
|
+
# now; the module alias in capybara/screenshot/diff/drivers.rb makes it
|
|
5
|
+
# reachable as Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver.
|
|
6
|
+
require "capybara/screenshot/diff/drivers"
|
|
7
|
+
require "snap_diff/drivers/chunky_png_driver"
|
|
@@ -1,171 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
require "capybara/screenshot/diff/drivers/base_driver"
|
|
11
|
-
|
|
12
|
-
module Capybara
|
|
13
|
-
module Screenshot
|
|
14
|
-
module Diff
|
|
15
|
-
# Compare two images and determine if they are equal, different, or within some comparison
|
|
16
|
-
# range considering color values and difference area size.
|
|
17
|
-
module Drivers
|
|
18
|
-
class VipsDriver < BaseDriver
|
|
19
|
-
def find_difference_region(comparison)
|
|
20
|
-
new_image, base_image, options = comparison.new_image, comparison.base_image, comparison.options
|
|
21
|
-
|
|
22
|
-
diff_mask = if options[:perceptual_threshold]
|
|
23
|
-
self.class.perceptual_difference_mask(base_image, new_image, options[:perceptual_threshold])
|
|
24
|
-
else
|
|
25
|
-
self.class.difference_mask(base_image, new_image, options[:color_distance_limit])
|
|
26
|
-
end
|
|
27
|
-
region = self.class.difference_region_by(diff_mask)
|
|
28
|
-
# TODO: schedule research when we got this case for VIPs
|
|
29
|
-
# region = nil if region && region_covers_entire_image?(region, base_image)
|
|
30
|
-
|
|
31
|
-
result = Difference.new(region, {}, comparison)
|
|
32
|
-
|
|
33
|
-
unless result.blank?
|
|
34
|
-
result.meta[:difference_level] = difference_level(diff_mask, base_image) if comparison.options[:tolerance]
|
|
35
|
-
result.meta[:diff_mask] = diff_mask
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
result
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def crop(region, i)
|
|
42
|
-
i.crop(*region.to_top_left_corner_coordinates)
|
|
43
|
-
rescue Vips::Error => e
|
|
44
|
-
warn(
|
|
45
|
-
"[capybara-screenshot-diff] Crop has been failed for " \
|
|
46
|
-
"{ region: #{region.to_top_left_corner_coordinates.inspect}, image: #{dimension(i).join("x")} }"
|
|
47
|
-
)
|
|
48
|
-
raise e
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def filter_image_with_median(image, median_filter_window_size)
|
|
52
|
-
image.median(median_filter_window_size)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def add_black_box(memo, region)
|
|
56
|
-
return memo unless region
|
|
57
|
-
|
|
58
|
-
memo.draw_rect([0, 0, 0, 0], *region.to_top_left_corner_coordinates, fill: true)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def difference_level(diff_mask, old_img, _region = nil)
|
|
62
|
-
self.class.difference_area_size_by(diff_mask).to_f / image_area_size(old_img)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
MAX_FILENAME_LENGTH = 200
|
|
66
|
-
|
|
67
|
-
# Vips could not work with the same file. Per each process we require to create new file
|
|
68
|
-
def save_image_to(image, filename)
|
|
69
|
-
# Dir::Tmpname will happily produce tempfile names that are too long for most unix filesystems,
|
|
70
|
-
# which leads to "unix error: File name too long". Apply a limit to avoid this.
|
|
71
|
-
limited_filename = filename.to_s[-MAX_FILENAME_LENGTH..] || filename.to_s
|
|
72
|
-
::Dir::Tmpname.create([limited_filename, PNG_EXTENSION]) do |tmp_image_filename|
|
|
73
|
-
image.write_to_file(tmp_image_filename)
|
|
74
|
-
FileUtils.mv(tmp_image_filename, filename)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def resize_image_to(image, new_width, new_height)
|
|
79
|
-
image.resize(new_width.to_f / image.width, vscale: new_height.to_f / image.height)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def load_images(old_file_name, new_file_name)
|
|
83
|
-
[from_file(old_file_name), from_file(new_file_name)]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def from_file(filename)
|
|
87
|
-
result = ::Vips::Image.new_from_file(filename.to_s)
|
|
88
|
-
|
|
89
|
-
result = result.colourspace(:srgb) if result.bands < 3
|
|
90
|
-
result = result.bandjoin(255) if result.bands == 3
|
|
91
|
-
|
|
92
|
-
result
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def draw_rectangles(images, region, rgba, offset: 0)
|
|
96
|
-
images.map do |image|
|
|
97
|
-
image.draw_rect(rgba, region.left - offset, region.top - offset, region.width + (offset * 2), region.height + (offset * 2))
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def same_pixels?(comparison)
|
|
102
|
-
(comparison.new_image == comparison.base_image).min == 255
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def merge(new_image, base_image)
|
|
106
|
-
base_image.composite2(new_image, :over)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def highlight_mask(diff_mask, merged_image, color: CapybaraScreenshotDiff::RED_RGBA)
|
|
110
|
-
diff_mask.ifthenelse(color, merged_image * 0.75)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
private
|
|
114
|
-
|
|
115
|
-
class << self
|
|
116
|
-
def difference_area(old_image, new_image, color_distance: 0)
|
|
117
|
-
mask = difference_mask(new_image, old_image, color_distance)
|
|
118
|
-
difference_area_size_by(mask)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def difference_area_size_by(difference_mask)
|
|
122
|
-
diff_mask = difference_mask == 0
|
|
123
|
-
diff_mask.hist_find.to_a[0][0].max
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def difference_mask(base_image, new_image, color_distance = nil)
|
|
127
|
-
result = (new_image - base_image).abs
|
|
128
|
-
color_distance ? result > color_distance : result
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def perceptual_difference_mask(base_image, new_image, threshold = 2.0)
|
|
132
|
-
color_diff = perceptual_color_diff(base_image, new_image) > threshold
|
|
133
|
-
alpha_diff = alpha_channel_diff(base_image, new_image)
|
|
134
|
-
alpha_diff ? (color_diff | alpha_diff) : color_diff
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def perceptual_color_diff(base_image, new_image)
|
|
138
|
-
base_rgb = (base_image.bands > 3) ? base_image.extract_band(0, n: 3) : base_image
|
|
139
|
-
new_rgb = (new_image.bands > 3) ? new_image.extract_band(0, n: 3) : new_image
|
|
140
|
-
base_lab = base_rgb.colourspace(:lab)
|
|
141
|
-
new_lab = new_rgb.colourspace(:lab)
|
|
142
|
-
base_lab.dE00(new_lab)
|
|
143
|
-
rescue Vips::Error
|
|
144
|
-
base_lab.dE76(new_lab)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def alpha_channel_diff(base_image, new_image)
|
|
148
|
-
return unless base_image.bands > 3 && new_image.bands > 3
|
|
149
|
-
|
|
150
|
-
(base_image.extract_band(3) - new_image.extract_band(3)).abs > 0
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def difference_region_by(diff_mask)
|
|
154
|
-
columns, rows = diff_mask.bandor.project
|
|
155
|
-
|
|
156
|
-
left = columns.profile[1].min
|
|
157
|
-
right = columns.width - columns.flip(:horizontal).profile[1].min
|
|
158
|
-
|
|
159
|
-
top = rows.profile[0].min
|
|
160
|
-
bottom = rows.height - rows.flip(:vertical).profile[0].min
|
|
161
|
-
|
|
162
|
-
return nil if right < left || bottom < top
|
|
163
|
-
|
|
164
|
-
Region.from_edge_coordinates(left, top, right, bottom)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
3
|
+
# Forwarder (ADR-004 v2 step 4): VipsDriver lives in SnapDiff::Drivers now;
|
|
4
|
+
# the module alias in capybara/screenshot/diff/drivers.rb makes it reachable
|
|
5
|
+
# as Capybara::Screenshot::Diff::Drivers::VipsDriver.
|
|
6
|
+
require "capybara/screenshot/diff/drivers"
|
|
7
|
+
require "snap_diff/drivers/vips_driver"
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Utils.find_driver_class_for(driver_option).new
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
3
|
+
# Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::Diff::Drivers now
|
|
4
|
+
# resolves lazily via snap_diff/legacy_shims' const_missing, with a
|
|
5
|
+
# deprecation warning pointing at SnapDiff::Drivers. The forwarded module is
|
|
6
|
+
# the same object, so Drivers.for and the Drivers::VipsDriver /
|
|
7
|
+
# Drivers::ChunkyPNGDriver constants keep resolving through the old name.
|
|
8
|
+
require "snap_diff/drivers"
|
|
9
|
+
require "snap_diff/legacy_shims"
|