capybara-screenshot-diff 1.1.0 → 1.4.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.
- checksums.yaml +4 -4
- data/.gitattributes +4 -0
- data/.github/workflows/test.yml +129 -0
- data/.gitignore +1 -1
- data/.standard.yml +11 -0
- data/Dockerfile +60 -0
- data/README.md +107 -4
- data/Rakefile +10 -8
- data/bin/console +3 -3
- data/bin/install-vips +11 -0
- data/bin/standardrb +29 -0
- data/capybara-screenshot-diff.gemspec +18 -26
- data/gemfiles/rails42.gemfile +4 -2
- data/gemfiles/rails50.gemfile +3 -2
- data/gemfiles/rails51.gemfile +3 -2
- data/gemfiles/rails52.gemfile +3 -2
- data/gemfiles/rails60_gems.rb +8 -0
- data/gemfiles/rails61_gems.rb +7 -0
- data/gems.rb +29 -0
- data/lib/capybara/screenshot/diff.rb +24 -14
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +355 -0
- data/lib/capybara/screenshot/diff/drivers/utils.rb +24 -0
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +180 -0
- data/lib/capybara/screenshot/diff/image_compare.rb +144 -288
- data/lib/capybara/screenshot/diff/os.rb +7 -7
- data/lib/capybara/screenshot/diff/stabilization.rb +92 -43
- data/lib/capybara/screenshot/diff/test_methods.rb +47 -52
- data/lib/capybara/screenshot/diff/vcs.rb +8 -3
- data/lib/capybara/screenshot/diff/version.rb +1 -1
- data/matrix_test.rb +20 -22
- metadata +20 -111
- data/.rubocop.yml +0 -62
- data/.rubocop_todo.yml +0 -52
- data/.travis.yml +0 -33
- data/Gemfile +0 -6
- data/gemfiles/common.gemfile +0 -12
- data/gemfiles/rails60.gemfile +0 -5
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara
|
4
|
+
module Screenshot
|
5
|
+
module Diff
|
6
|
+
module Utils
|
7
|
+
def self.detect_available_drivers
|
8
|
+
result = []
|
9
|
+
begin
|
10
|
+
result << :vips if defined?(Vips) || require("vips")
|
11
|
+
rescue LoadError
|
12
|
+
# vips not present
|
13
|
+
end
|
14
|
+
begin
|
15
|
+
result << :chunky_png if defined?(ChunkyPNG) || require("chunky_png")
|
16
|
+
rescue LoadError
|
17
|
+
# chunky_png not present
|
18
|
+
end
|
19
|
+
result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "vips"
|
5
|
+
rescue LoadError => e
|
6
|
+
warn 'Required ruby-vips gem is missing. Add `gem "ruby-vips"` to Gemfile' if e.message.include?("vips")
|
7
|
+
raise
|
8
|
+
end
|
9
|
+
|
10
|
+
require_relative "./chunky_png_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
|
19
|
+
attr_reader :new_file_name, :old_file_name, :options
|
20
|
+
|
21
|
+
def initialize(new_file_name, old_file_name = nil, **options)
|
22
|
+
@new_file_name = new_file_name
|
23
|
+
@old_file_name = old_file_name || "#{new_file_name}~"
|
24
|
+
|
25
|
+
@options = options || {}
|
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
|
+
end
|
34
|
+
|
35
|
+
def shift_distance_equal?
|
36
|
+
warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \
|
37
|
+
"please use median_filter_window_size and color_distance_limit options"
|
38
|
+
chunky_png_comparator.quick_equal?
|
39
|
+
end
|
40
|
+
|
41
|
+
def shift_distance_different?
|
42
|
+
warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \
|
43
|
+
"please use median_filter_window_size and color_distance_limit options"
|
44
|
+
chunky_png_comparator.different?
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_difference_region(new_image, old_image, color_distance_limit, _shift_distance_limit, _area_size_limit, fast_fail: false)
|
48
|
+
diff_mask = VipsUtil.difference_mask(color_distance_limit, old_image, new_image)
|
49
|
+
region = VipsUtil.difference_region_by(diff_mask)
|
50
|
+
|
51
|
+
[region, diff_mask]
|
52
|
+
end
|
53
|
+
|
54
|
+
def size(region)
|
55
|
+
return 0 unless region
|
56
|
+
|
57
|
+
(region[2] - region[0]) * (region[3] - region[1])
|
58
|
+
end
|
59
|
+
|
60
|
+
def adds_error_details_to(_log)
|
61
|
+
end
|
62
|
+
|
63
|
+
# old private
|
64
|
+
|
65
|
+
def inscribed?(dimensions, i)
|
66
|
+
dimension(i) == dimensions || i.width < dimensions[0] || i.height < dimensions[1]
|
67
|
+
end
|
68
|
+
|
69
|
+
def crop(dimensions, i)
|
70
|
+
i.crop(0, 0, *dimensions)
|
71
|
+
end
|
72
|
+
|
73
|
+
def filter_image_with_median(image, median_filter_window_size)
|
74
|
+
image.median(median_filter_window_size)
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_black_box(memo, region)
|
78
|
+
memo.draw_rect([0, 0, 0, 0], *region, fill: true)
|
79
|
+
end
|
80
|
+
|
81
|
+
def chunky_png_comparator
|
82
|
+
@chunky_png_comparator ||= ImageCompare.new(
|
83
|
+
@new_file_name,
|
84
|
+
@old_file_name,
|
85
|
+
@options.merge(driver: :chunky_png, tolerance: nil, median_filter_window_size: nil)
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def difference_level(diff_mask, old_img, _region = nil)
|
90
|
+
VipsUtil.difference_area_size_by(diff_mask).to_f / image_area_size(old_img)
|
91
|
+
end
|
92
|
+
|
93
|
+
def image_area_size(old_img)
|
94
|
+
width_for(old_img) * height_for(old_img)
|
95
|
+
end
|
96
|
+
|
97
|
+
def height_for(image)
|
98
|
+
image.height
|
99
|
+
end
|
100
|
+
|
101
|
+
def width_for(image)
|
102
|
+
image.width
|
103
|
+
end
|
104
|
+
|
105
|
+
def save_image_to(image, filename)
|
106
|
+
image.write_to_file(filename)
|
107
|
+
end
|
108
|
+
|
109
|
+
def resize_image_to(image, new_width, new_height)
|
110
|
+
image.resize(1.* new_width / new_height)
|
111
|
+
end
|
112
|
+
|
113
|
+
def load_images(old_file_name, new_file_name, driver = self)
|
114
|
+
[driver.from_file(old_file_name), driver.from_file(new_file_name)]
|
115
|
+
end
|
116
|
+
|
117
|
+
def from_file(filename)
|
118
|
+
result = ::Vips::Image.new_from_file(filename)
|
119
|
+
|
120
|
+
result = result.colourspace("srgb") if result.bands < 3
|
121
|
+
result = result.bandjoin(255) if result.bands == 3
|
122
|
+
|
123
|
+
result
|
124
|
+
end
|
125
|
+
|
126
|
+
def dimension_changed?(org_image, new_image)
|
127
|
+
return false if dimension(org_image) == dimension(new_image)
|
128
|
+
|
129
|
+
change_msg = [org_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
|
130
|
+
warn "Image size has changed for #{@new_file_name}: #{change_msg}"
|
131
|
+
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
def dimension(image)
|
136
|
+
[image.width, image.height]
|
137
|
+
end
|
138
|
+
|
139
|
+
def draw_rectangles(images, (left, top, right, bottom), rgba)
|
140
|
+
images.map do |image|
|
141
|
+
image.draw_rect(rgba, left - 1, top - 1, right - left + 2, bottom - top + 2)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class VipsUtil
|
146
|
+
def self.difference(old_image, new_image, color_distance: 0)
|
147
|
+
diff_mask = difference_mask(color_distance, new_image, old_image)
|
148
|
+
difference_region_by(diff_mask)
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.difference_area(old_image, new_image, color_distance: 0)
|
152
|
+
difference_mask = difference_mask(color_distance, new_image, old_image)
|
153
|
+
difference_area_size_by(difference_mask)
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.difference_area_size_by(difference_mask)
|
157
|
+
diff_mask = difference_mask == 0
|
158
|
+
diff_mask.hist_find.to_a[0][0].max
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.difference_mask(color_distance, old_image, new_image)
|
162
|
+
(new_image - old_image).abs > color_distance
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.difference_region_by(diff_mask)
|
166
|
+
columns, rows = diff_mask.project
|
167
|
+
|
168
|
+
left = columns.profile[1].min
|
169
|
+
right = columns.width - columns.flip("horizontal").profile[1].min
|
170
|
+
top = rows.profile[0].min
|
171
|
+
bottom = rows.height - rows.flip("vertical").profile[0].min
|
172
|
+
|
173
|
+
[left, top, right, bottom]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -1,65 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'chunky_png'
|
4
|
-
|
5
3
|
module Capybara
|
6
4
|
module Screenshot
|
7
5
|
module Diff
|
8
|
-
|
6
|
+
LOADED_DRIVERS = {}
|
7
|
+
|
8
|
+
# Compare two images and determine if they are equal, different, or within some comparison
|
9
9
|
# range considering color values and difference area size.
|
10
|
-
class ImageCompare
|
11
|
-
|
10
|
+
class ImageCompare < SimpleDelegator
|
11
|
+
attr_reader :driver, :driver_options
|
12
12
|
|
13
|
-
attr_reader :annotated_new_file_name, :annotated_old_file_name, :
|
13
|
+
attr_reader :annotated_new_file_name, :annotated_old_file_name, :area_size_limit,
|
14
|
+
:color_distance_limit, :new_file_name, :old_file_name, :shift_distance_limit,
|
15
|
+
:skip_area
|
14
16
|
|
15
|
-
def initialize(new_file_name, old_file_name = nil,
|
16
|
-
area_size_limit: nil, shift_distance_limit: nil, skip_area: nil)
|
17
|
+
def initialize(new_file_name, old_file_name = nil, **driver_options)
|
17
18
|
@new_file_name = new_file_name
|
18
|
-
@color_distance_limit = color_distance_limit
|
19
|
-
@area_size_limit = area_size_limit
|
20
|
-
@shift_distance_limit = shift_distance_limit
|
21
|
-
@dimensions = dimensions
|
22
|
-
@skip_area = skip_area
|
23
19
|
@old_file_name = old_file_name || "#{new_file_name}~"
|
24
|
-
@annotated_old_file_name = "#{new_file_name.chomp(
|
25
|
-
@annotated_new_file_name = "#{new_file_name.chomp(
|
26
|
-
reset
|
27
|
-
end
|
20
|
+
@annotated_old_file_name = "#{new_file_name.chomp(".png")}.committed.png"
|
21
|
+
@annotated_new_file_name = "#{new_file_name.chomp(".png")}.latest.png"
|
28
22
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
@
|
33
|
-
@
|
34
|
-
@
|
23
|
+
@driver_options = driver_options
|
24
|
+
|
25
|
+
@color_distance_limit = driver_options[:color_distance_limit] || 0
|
26
|
+
@area_size_limit = driver_options[:area_size_limit]
|
27
|
+
@shift_distance_limit = driver_options[:shift_distance_limit]
|
28
|
+
@dimensions = driver_options[:dimensions]
|
29
|
+
@skip_area = driver_options[:skip_area]
|
30
|
+
@tolerance = driver_options[:tolerance]
|
31
|
+
@median_filter_window_size = driver_options[:median_filter_window_size]
|
32
|
+
|
33
|
+
driver_klass = find_driver_class_for(@driver_options.fetch(:driver, :chunky_png))
|
34
|
+
@driver = driver_klass.new(@new_file_name, @old_file_name, **@driver_options)
|
35
|
+
|
36
|
+
super(@driver)
|
35
37
|
end
|
36
38
|
|
37
39
|
# Compare the two image files and return `true` or `false` as quickly as possible.
|
38
40
|
# Return falsish if the old file does not exist or the image dimensions do not match.
|
39
41
|
def quick_equal?
|
40
|
-
return
|
42
|
+
return false unless old_file_exists?
|
41
43
|
return true if new_file_size == old_file_size
|
42
44
|
|
43
|
-
old_bytes, new_bytes = load_image_files(@old_file_name, @new_file_name)
|
44
|
-
return true if old_bytes == new_bytes
|
45
|
+
# old_bytes, new_bytes = load_image_files(@old_file_name, @new_file_name)
|
46
|
+
# return true if old_bytes == new_bytes
|
45
47
|
|
46
|
-
images = load_images(
|
47
|
-
|
48
|
-
crop_images(images, @dimensions) if @dimensions
|
48
|
+
images = driver.load_images(@old_file_name, @new_file_name)
|
49
|
+
old_image, new_image = preprocess_images(images, driver)
|
49
50
|
|
50
|
-
return false if
|
51
|
-
return true if images.first.pixels == images.last.pixels
|
51
|
+
return false if driver.dimension_changed?(old_image, new_image)
|
52
52
|
|
53
|
-
|
53
|
+
region, meta = driver.find_difference_region(
|
54
|
+
new_image,
|
55
|
+
old_image,
|
56
|
+
@color_distance_limit,
|
57
|
+
@shift_distance_limit,
|
58
|
+
@area_size_limit,
|
59
|
+
fast_fail: true
|
60
|
+
)
|
54
61
|
|
55
|
-
|
62
|
+
self.difference_region = region
|
56
63
|
|
57
|
-
return true if
|
64
|
+
return true if difference_region_empty?(new_image, region)
|
58
65
|
|
59
|
-
if @area_size_limit
|
60
|
-
|
61
|
-
|
62
|
-
|
66
|
+
return true if @area_size_limit && driver.size(region) <= @area_size_limit
|
67
|
+
|
68
|
+
return true if @tolerance && @tolerance >= driver.difference_level(meta, old_image, region)
|
69
|
+
|
70
|
+
# TODO: Remove this or find similar solution for vips
|
71
|
+
return true if @shift_distance_limit && driver.shift_distance_equal?
|
63
72
|
|
64
73
|
false
|
65
74
|
end
|
@@ -70,313 +79,160 @@ def quick_equal?
|
|
70
79
|
def different?
|
71
80
|
return nil unless old_file_exists?
|
72
81
|
|
73
|
-
|
82
|
+
images = driver.load_images(@old_file_name, @new_file_name)
|
74
83
|
|
75
|
-
|
84
|
+
old_image, new_image = preprocess_images(images, driver)
|
76
85
|
|
77
|
-
|
86
|
+
if driver.dimension_changed?(old_image, new_image)
|
87
|
+
save(new_image, old_image, @annotated_new_file_name, @annotated_old_file_name)
|
78
88
|
|
79
|
-
|
89
|
+
self.difference_region = 0, 0, driver.width_for(old_image), driver.height_for(old_image)
|
80
90
|
|
81
|
-
old_img = images.first
|
82
|
-
new_img = images.last
|
83
|
-
|
84
|
-
if sizes_changed?(old_img, new_img)
|
85
|
-
save_images(@annotated_new_file_name, new_img, @annotated_old_file_name, old_img)
|
86
|
-
@left = 0
|
87
|
-
@top = 0
|
88
|
-
@right = old_img.dimension.width - 1
|
89
|
-
@bottom = old_img.dimension.height - 1
|
90
91
|
return true
|
91
92
|
end
|
92
93
|
|
93
|
-
|
94
|
+
region, meta = driver.find_difference_region(
|
95
|
+
new_image,
|
96
|
+
old_image,
|
97
|
+
@color_distance_limit,
|
98
|
+
@shift_distance_limit,
|
99
|
+
@area_size_limit
|
100
|
+
)
|
101
|
+
self.difference_region = region
|
94
102
|
|
95
|
-
|
103
|
+
return not_different if difference_region_empty?(old_image, region)
|
104
|
+
return not_different if @area_size_limit && driver.size(region) <= @area_size_limit
|
105
|
+
return not_different if @tolerance && @tolerance > driver.difference_level(meta, old_image, region)
|
96
106
|
|
97
|
-
|
98
|
-
return not_different if @
|
107
|
+
# TODO: Remove this or find similar solution for vips
|
108
|
+
return not_different if @shift_distance_limit && !driver.shift_distance_different?
|
99
109
|
|
100
|
-
|
110
|
+
annotate_and_save(images, region)
|
101
111
|
|
102
|
-
save_images(@annotated_new_file_name, annotated_new_img,
|
103
|
-
@annotated_old_file_name, annotated_old_img)
|
104
112
|
true
|
105
113
|
end
|
106
114
|
|
107
|
-
def
|
108
|
-
@old_file_name
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
@old_file_size ||= old_file_exists? && File.size(@old_file_name)
|
115
|
+
def clean_tmp_files
|
116
|
+
FileUtils.cp @old_file_name, @new_file_name if old_file_exists?
|
117
|
+
File.delete(@old_file_name) if old_file_exists?
|
118
|
+
File.delete(@annotated_old_file_name) if File.exist?(@annotated_old_file_name)
|
119
|
+
File.delete(@annotated_new_file_name) if File.exist?(@annotated_new_file_name)
|
113
120
|
end
|
114
121
|
|
115
|
-
|
116
|
-
|
117
|
-
end
|
122
|
+
DIFF_COLOR = [255, 0, 0, 255].freeze
|
123
|
+
SKIP_COLOR = [255, 192, 0, 255].freeze
|
118
124
|
|
119
|
-
def
|
120
|
-
|
125
|
+
def annotate_and_save(images, region = difference_region)
|
126
|
+
annotated_images = driver.draw_rectangles(images, region, DIFF_COLOR)
|
127
|
+
@skip_area.to_a.flatten.each_slice(4) do |region|
|
128
|
+
annotated_images = driver.draw_rectangles(annotated_images, region, SKIP_COLOR)
|
129
|
+
end
|
130
|
+
save(*annotated_images, @annotated_new_file_name, @annotated_old_file_name)
|
121
131
|
end
|
122
132
|
|
123
|
-
def
|
124
|
-
(
|
133
|
+
def save(new_img, old_img, annotated_new_file_name, annotated_old_file_name)
|
134
|
+
driver.save_image_to(old_img, annotated_old_file_name)
|
135
|
+
driver.save_image_to(new_img, annotated_new_file_name)
|
125
136
|
end
|
126
137
|
|
127
|
-
def
|
128
|
-
|
129
|
-
@max_color_distance
|
138
|
+
def old_file_exists?
|
139
|
+
@old_file_name && File.exist?(@old_file_name)
|
130
140
|
end
|
131
141
|
|
132
|
-
def
|
133
|
-
|
134
|
-
|
142
|
+
def reset
|
143
|
+
self.difference_region = nil
|
144
|
+
driver.reset
|
135
145
|
end
|
136
146
|
|
137
|
-
|
147
|
+
def error_message
|
148
|
+
result = {
|
149
|
+
area_size: driver.size(difference_region),
|
150
|
+
region: difference_region
|
151
|
+
}
|
138
152
|
|
139
|
-
|
140
|
-
old_file, new_file = load_image_files(@old_file_name, @new_file_name)
|
141
|
-
if old_file == new_file
|
142
|
-
@max_color_distance = 0
|
143
|
-
@max_shift_distance = 0
|
144
|
-
return
|
145
|
-
end
|
153
|
+
driver.adds_error_details_to(result)
|
146
154
|
|
147
|
-
|
148
|
-
calculate_max_color_distance(new_image, old_image)
|
149
|
-
calculate_max_shift_limit(new_image, old_image)
|
155
|
+
["(#{result.to_json})", new_file_name, annotated_old_file_name, annotated_new_file_name].join("\n")
|
150
156
|
end
|
151
157
|
|
152
|
-
def
|
153
|
-
|
154
|
-
@max_color_distance = pixel_pairs.inject(0) do |max, (p1, p2)|
|
155
|
-
next max unless p1 && p2
|
158
|
+
def difference_region
|
159
|
+
return nil unless @left || @top || @right || @bottom
|
156
160
|
|
157
|
-
|
158
|
-
[max, d].max
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def calculate_max_shift_limit(new_img, old_img)
|
163
|
-
(0...new_img.width).each do |x|
|
164
|
-
(0...new_img.height).each do |y|
|
165
|
-
shift_distance =
|
166
|
-
shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
|
167
|
-
if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
|
168
|
-
@max_shift_distance = shift_distance
|
169
|
-
return if @max_shift_distance == Float::INFINITY # rubocop: disable Lint/NonLocalExitFromIterator
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def not_different
|
176
|
-
clean_tmp_files
|
177
|
-
false
|
178
|
-
end
|
179
|
-
|
180
|
-
def save_images(new_file_name, new_img, org_file_name, org_img)
|
181
|
-
org_img.save(org_file_name)
|
182
|
-
new_img.save(new_file_name)
|
161
|
+
[@left, @top, @right, @bottom]
|
183
162
|
end
|
184
163
|
|
185
|
-
|
186
|
-
FileUtils.cp @old_file_name, @new_file_name
|
187
|
-
File.delete(@old_file_name) if File.exist?(@old_file_name)
|
188
|
-
File.delete(@annotated_old_file_name) if File.exist?(@annotated_old_file_name)
|
189
|
-
File.delete(@annotated_new_file_name) if File.exist?(@annotated_new_file_name)
|
190
|
-
end
|
164
|
+
private
|
191
165
|
|
192
|
-
def
|
193
|
-
|
166
|
+
def find_driver_class_for(driver)
|
167
|
+
driver = AVAILABLE_DRIVERS.first if driver == :auto
|
168
|
+
|
169
|
+
LOADED_DRIVERS[driver] ||=
|
170
|
+
case driver
|
171
|
+
when :chunky_png
|
172
|
+
require "capybara/screenshot/diff/drivers/chunky_png_driver"
|
173
|
+
Drivers::ChunkyPNGDriver
|
174
|
+
when :vips
|
175
|
+
require "capybara/screenshot/diff/drivers/vips_driver"
|
176
|
+
Drivers::VipsDriver
|
177
|
+
else
|
178
|
+
fail "Wrong adapter #{driver.inspect}. Available adapters: #{AVAILABLE_DRIVERS.inspect}"
|
179
|
+
end
|
194
180
|
end
|
195
181
|
|
196
|
-
def
|
197
|
-
|
198
|
-
new_file = File.binread(file_name)
|
199
|
-
[old_file, new_file]
|
182
|
+
def old_file_size
|
183
|
+
@old_file_size ||= old_file_exists? && File.size(@old_file_name)
|
200
184
|
end
|
201
185
|
|
202
|
-
def
|
203
|
-
|
204
|
-
|
205
|
-
change_msg = [org_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(' => ')
|
206
|
-
puts "Image size has changed for #{@new_file_name}: #{change_msg}"
|
207
|
-
true
|
186
|
+
def new_file_size
|
187
|
+
File.size(@new_file_name)
|
208
188
|
end
|
209
189
|
|
210
|
-
def
|
211
|
-
|
212
|
-
|
213
|
-
i
|
214
|
-
else
|
215
|
-
i.crop(0, 0, *dimensions)
|
216
|
-
end
|
217
|
-
end
|
190
|
+
def not_different
|
191
|
+
clean_tmp_files
|
192
|
+
false
|
218
193
|
end
|
219
194
|
|
220
|
-
def
|
221
|
-
|
222
|
-
new_img = image.dup
|
223
|
-
new_img.rect(left - 1, top - 1, right + 1, bottom + 1, ChunkyPNG::Color.rgb(255, 0, 0))
|
224
|
-
new_img
|
225
|
-
end
|
195
|
+
def load_images(old_file_name, new_file_name, driver = self)
|
196
|
+
[driver.from_file(old_file_name), driver.from_file(new_file_name)]
|
226
197
|
end
|
227
198
|
|
228
|
-
def
|
229
|
-
|
230
|
-
|
231
|
-
[left, top, right, bottom]
|
232
|
-
end
|
199
|
+
def preprocess_images(images, driver = self)
|
200
|
+
old_img = preprocess_image(images.first, driver)
|
201
|
+
new_img = preprocess_image(images.last, driver)
|
233
202
|
|
234
|
-
|
235
|
-
old_img.height.times do |y|
|
236
|
-
old_img.width.times do |x|
|
237
|
-
return [x, y, x, y] unless same_color?(old_img, new_img, x, y)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
nil
|
203
|
+
[old_img, new_img]
|
241
204
|
end
|
242
205
|
|
243
|
-
def
|
244
|
-
|
245
|
-
bottom = @bottom
|
246
|
-
left = @left || old_img.width - 1
|
247
|
-
right = @right || 0
|
248
|
-
old_img.height.times do |y|
|
249
|
-
(0...left).find do |x|
|
250
|
-
next if same_color?(old_img, new_img, x, y)
|
251
|
-
|
252
|
-
top ||= y
|
253
|
-
bottom = y
|
254
|
-
left = x
|
255
|
-
right = x if x > right
|
256
|
-
x
|
257
|
-
end
|
258
|
-
(old_img.width - 1).step(right + 1, -1).find do |x|
|
259
|
-
unless same_color?(old_img, new_img, x, y)
|
260
|
-
bottom = y
|
261
|
-
right = x
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
[left, top, right, bottom]
|
266
|
-
end
|
206
|
+
def preprocess_image(image, driver = self)
|
207
|
+
result = image
|
267
208
|
|
268
|
-
|
269
|
-
|
270
|
-
(old_img.height - 1).step(bottom + 1, -1).find do |y|
|
271
|
-
(left..right).find do |x|
|
272
|
-
bottom = y unless same_color?(old_img, new_img, x, y)
|
273
|
-
end
|
274
|
-
end
|
209
|
+
if @dimensions && driver.inscribed?(@dimensions, result)
|
210
|
+
result = driver.crop(@dimensions, result)
|
275
211
|
end
|
276
|
-
bottom
|
277
|
-
end
|
278
212
|
|
279
|
-
|
280
|
-
|
281
|
-
return true if skip_start_x <= x && x <= skip_end_x && skip_start_y <= y && y <= skip_end_y
|
213
|
+
if @median_filter_window_size
|
214
|
+
result = driver.filter_image_with_median(image, @median_filter_window_size)
|
282
215
|
end
|
283
216
|
|
284
|
-
|
285
|
-
|
286
|
-
if !@max_color_distance || color_distance > @max_color_distance
|
287
|
-
@max_color_distance = color_distance
|
217
|
+
if @skip_area
|
218
|
+
result = @skip_area.reduce(result) { |image, region| driver.add_black_box(image, region) }
|
288
219
|
end
|
289
|
-
color_matches = color_distance == 0 || (@color_distance_limit && @color_distance_limit > 0 &&
|
290
|
-
color_distance <= @color_distance_limit)
|
291
|
-
return color_matches if !@shift_distance_limit || @max_shift_distance == Float::INFINITY
|
292
|
-
|
293
|
-
shift_distance = (color_matches && 0) ||
|
294
|
-
shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
|
295
|
-
if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
|
296
|
-
@max_shift_distance = shift_distance
|
297
|
-
end
|
298
|
-
color_matches
|
299
|
-
end
|
300
220
|
|
301
|
-
|
302
|
-
org_color = old_img[x, y]
|
303
|
-
if shift_distance_limit
|
304
|
-
start_x = [0, x - shift_distance_limit].max
|
305
|
-
end_x = [x + shift_distance_limit, new_img.width - 1].min
|
306
|
-
xs = (start_x..end_x).to_a
|
307
|
-
start_y = [0, y - shift_distance_limit].max
|
308
|
-
end_y = [y + shift_distance_limit, new_img.height - 1].min
|
309
|
-
ys = (start_y..end_y).to_a
|
310
|
-
new_pixels = xs.product(ys)
|
311
|
-
distances = new_pixels.map do |dx, dy|
|
312
|
-
new_color = new_img[dx, dy]
|
313
|
-
ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
|
314
|
-
end
|
315
|
-
distances.min
|
316
|
-
else
|
317
|
-
ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
|
318
|
-
end
|
221
|
+
result
|
319
222
|
end
|
320
223
|
|
321
|
-
def
|
322
|
-
|
323
|
-
shift_distance = 0
|
324
|
-
loop do
|
325
|
-
bounds_breached = 0
|
326
|
-
top_row = y - shift_distance
|
327
|
-
if top_row >= 0 # top
|
328
|
-
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
329
|
-
if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
|
330
|
-
return shift_distance
|
331
|
-
end
|
332
|
-
end
|
333
|
-
else
|
334
|
-
bounds_breached += 1
|
335
|
-
end
|
336
|
-
if shift_distance > 0
|
337
|
-
if (x - shift_distance) >= 0 # left
|
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
|
342
|
-
end
|
343
|
-
end
|
344
|
-
else
|
345
|
-
bounds_breached += 1
|
346
|
-
end
|
347
|
-
if (y + shift_distance) < new_img.height # bottom
|
348
|
-
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
349
|
-
if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
|
350
|
-
return shift_distance
|
351
|
-
end
|
352
|
-
end
|
353
|
-
else
|
354
|
-
bounds_breached += 1
|
355
|
-
end
|
356
|
-
if (x + shift_distance) < new_img.width # right
|
357
|
-
([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
|
358
|
-
.each do |dy|
|
359
|
-
if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
|
360
|
-
return shift_distance
|
361
|
-
end
|
362
|
-
end
|
363
|
-
else
|
364
|
-
bounds_breached += 1
|
365
|
-
end
|
366
|
-
end
|
367
|
-
break if bounds_breached == 4
|
368
|
-
|
369
|
-
shift_distance += 1
|
370
|
-
end
|
371
|
-
Float::INFINITY
|
224
|
+
def difference_region=(region)
|
225
|
+
@left, @top, @right, @bottom = region
|
372
226
|
end
|
373
227
|
|
374
|
-
def
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
228
|
+
def difference_region_empty?(new_image, region)
|
229
|
+
region.nil? ||
|
230
|
+
(
|
231
|
+
region[1] == height_for(new_image) &&
|
232
|
+
region[0] == width_for(new_image) &&
|
233
|
+
region[2].zero? &&
|
234
|
+
region[3].zero?
|
235
|
+
)
|
380
236
|
end
|
381
237
|
end
|
382
238
|
end
|