capybara-screenshot-diff 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +86 -2
- 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 +142 -293
- data/lib/capybara/screenshot/diff/os.rb +7 -7
- data/lib/capybara/screenshot/diff/stabilization.rb +78 -46
- data/lib/capybara/screenshot/diff/test_methods.rb +46 -55
- 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 -65
- data/.rubocop_todo.yml +0 -52
- data/.travis.yml +0 -30
- 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,66 +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
|
6
|
+
LOADED_DRIVERS = {}
|
7
|
+
|
8
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
13
|
attr_reader :annotated_new_file_name, :annotated_old_file_name, :area_size_limit,
|
14
|
-
|
14
|
+
:color_distance_limit, :new_file_name, :old_file_name, :shift_distance_limit,
|
15
|
+
:skip_area
|
15
16
|
|
16
|
-
def initialize(new_file_name, old_file_name = nil,
|
17
|
-
area_size_limit: nil, shift_distance_limit: nil, skip_area: nil)
|
17
|
+
def initialize(new_file_name, old_file_name = nil, **driver_options)
|
18
18
|
@new_file_name = new_file_name
|
19
|
-
@color_distance_limit = color_distance_limit
|
20
|
-
@area_size_limit = area_size_limit
|
21
|
-
@shift_distance_limit = shift_distance_limit
|
22
|
-
@dimensions = dimensions
|
23
|
-
@skip_area = skip_area
|
24
19
|
@old_file_name = old_file_name || "#{new_file_name}~"
|
25
|
-
@annotated_old_file_name = "#{new_file_name.chomp(
|
26
|
-
@annotated_new_file_name = "#{new_file_name.chomp(
|
27
|
-
reset
|
28
|
-
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"
|
29
22
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@
|
34
|
-
@
|
35
|
-
@
|
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)
|
36
37
|
end
|
37
38
|
|
38
39
|
# Compare the two image files and return `true` or `false` as quickly as possible.
|
39
40
|
# Return falsish if the old file does not exist or the image dimensions do not match.
|
40
41
|
def quick_equal?
|
41
|
-
return
|
42
|
+
return false unless old_file_exists?
|
42
43
|
return true if new_file_size == old_file_size
|
43
44
|
|
44
|
-
old_bytes, new_bytes = load_image_files(@old_file_name, @new_file_name)
|
45
|
-
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
|
46
47
|
|
47
|
-
images = load_images(
|
48
|
-
|
49
|
-
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)
|
50
50
|
|
51
|
-
return false if
|
52
|
-
return true if images.first.pixels == images.last.pixels
|
51
|
+
return false if driver.dimension_changed?(old_image, new_image)
|
53
52
|
|
54
|
-
|
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
|
+
)
|
55
61
|
|
56
|
-
|
62
|
+
self.difference_region = region
|
57
63
|
|
58
|
-
return true if
|
64
|
+
return true if difference_region_empty?(new_image, region)
|
59
65
|
|
60
|
-
if @area_size_limit
|
61
|
-
|
62
|
-
|
63
|
-
|
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?
|
64
72
|
|
65
73
|
false
|
66
74
|
end
|
@@ -71,319 +79,160 @@ def quick_equal?
|
|
71
79
|
def different?
|
72
80
|
return nil unless old_file_exists?
|
73
81
|
|
74
|
-
|
82
|
+
images = driver.load_images(@old_file_name, @new_file_name)
|
75
83
|
|
76
|
-
|
84
|
+
old_image, new_image = preprocess_images(images, driver)
|
77
85
|
|
78
|
-
|
86
|
+
if driver.dimension_changed?(old_image, new_image)
|
87
|
+
save(new_image, old_image, @annotated_new_file_name, @annotated_old_file_name)
|
79
88
|
|
80
|
-
|
89
|
+
self.difference_region = 0, 0, driver.width_for(old_image), driver.height_for(old_image)
|
81
90
|
|
82
|
-
old_img = images.first
|
83
|
-
new_img = images.last
|
84
|
-
|
85
|
-
if sizes_changed?(old_img, new_img)
|
86
|
-
save_images(@annotated_new_file_name, new_img, @annotated_old_file_name, old_img)
|
87
|
-
@left = 0
|
88
|
-
@top = 0
|
89
|
-
@right = old_img.dimension.width - 1
|
90
|
-
@bottom = old_img.dimension.height - 1
|
91
91
|
return true
|
92
92
|
end
|
93
93
|
|
94
|
-
|
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
|
95
102
|
|
96
|
-
|
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)
|
97
106
|
|
98
|
-
|
99
|
-
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?
|
100
109
|
|
101
|
-
|
102
|
-
true
|
103
|
-
end
|
110
|
+
annotate_and_save(images, region)
|
104
111
|
|
105
|
-
|
106
|
-
@old_file_name && File.exist?(@old_file_name)
|
107
|
-
end
|
108
|
-
|
109
|
-
def old_file_size
|
110
|
-
@old_file_size ||= old_file_exists? && File.size(@old_file_name)
|
112
|
+
true
|
111
113
|
end
|
112
114
|
|
113
|
-
def
|
114
|
-
|
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)
|
115
120
|
end
|
116
121
|
|
117
|
-
|
118
|
-
|
122
|
+
DIFF_COLOR = [255, 0, 0, 255].freeze
|
123
|
+
SKIP_COLOR = [255, 192, 0, 255].freeze
|
119
124
|
|
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
|
-
annotated_old_img, annotated_new_img = draw_rectangles(images, @bottom, @left, @right, @top)
|
153
|
+
driver.adds_error_details_to(result)
|
141
154
|
|
142
|
-
|
143
|
-
@annotated_old_file_name, annotated_old_img)
|
155
|
+
["(#{result.to_json})", new_file_name, annotated_old_file_name, annotated_new_file_name].join("\n")
|
144
156
|
end
|
145
157
|
|
146
|
-
def
|
147
|
-
|
148
|
-
if old_file == new_file
|
149
|
-
@max_color_distance = 0
|
150
|
-
@max_shift_distance = 0
|
151
|
-
return
|
152
|
-
end
|
158
|
+
def difference_region
|
159
|
+
return nil unless @left || @top || @right || @bottom
|
153
160
|
|
154
|
-
|
155
|
-
calculate_max_color_distance(new_image, old_image)
|
156
|
-
calculate_max_shift_limit(new_image, old_image)
|
161
|
+
[@left, @top, @right, @bottom]
|
157
162
|
end
|
158
163
|
|
159
|
-
|
160
|
-
pixel_pairs = old_image.pixels.zip(new_image.pixels)
|
161
|
-
@max_color_distance = pixel_pairs.inject(0) do |max, (p1, p2)|
|
162
|
-
next max unless p1 && p2
|
163
|
-
|
164
|
-
d = ChunkyPNG::Color.euclidean_distance_rgba(p1, p2)
|
165
|
-
[max, d].max
|
166
|
-
end
|
167
|
-
end
|
164
|
+
private
|
168
165
|
|
169
|
-
def
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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}"
|
178
179
|
end
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
def not_different
|
183
|
-
clean_tmp_files
|
184
|
-
false
|
185
|
-
end
|
186
|
-
|
187
|
-
def save_images(new_file_name, new_img, org_file_name, org_img)
|
188
|
-
org_img.save(org_file_name)
|
189
|
-
new_img.save(new_file_name)
|
190
|
-
end
|
191
|
-
|
192
|
-
def clean_tmp_files
|
193
|
-
FileUtils.cp @old_file_name, @new_file_name
|
194
|
-
File.delete(@old_file_name) if File.exist?(@old_file_name)
|
195
|
-
File.delete(@annotated_old_file_name) if File.exist?(@annotated_old_file_name)
|
196
|
-
File.delete(@annotated_new_file_name) if File.exist?(@annotated_new_file_name)
|
197
|
-
end
|
198
|
-
|
199
|
-
def load_images(old_file, new_file)
|
200
|
-
[ChunkyPNG::Image.from_blob(old_file), ChunkyPNG::Image.from_blob(new_file)]
|
201
180
|
end
|
202
181
|
|
203
|
-
def
|
204
|
-
|
205
|
-
new_file = File.binread(file_name)
|
206
|
-
[old_file, new_file]
|
182
|
+
def old_file_size
|
183
|
+
@old_file_size ||= old_file_exists? && File.size(@old_file_name)
|
207
184
|
end
|
208
185
|
|
209
|
-
def
|
210
|
-
|
211
|
-
|
212
|
-
change_msg = [org_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(' => ')
|
213
|
-
puts "Image size has changed for #{@new_file_name}: #{change_msg}"
|
214
|
-
true
|
186
|
+
def new_file_size
|
187
|
+
File.size(@new_file_name)
|
215
188
|
end
|
216
189
|
|
217
|
-
def
|
218
|
-
|
219
|
-
|
220
|
-
i
|
221
|
-
else
|
222
|
-
i.crop(0, 0, *dimensions)
|
223
|
-
end
|
224
|
-
end
|
190
|
+
def not_different
|
191
|
+
clean_tmp_files
|
192
|
+
false
|
225
193
|
end
|
226
194
|
|
227
|
-
def
|
228
|
-
|
229
|
-
new_img = image.dup
|
230
|
-
new_img.rect(left - 1, top - 1, right + 1, bottom + 1, ChunkyPNG::Color.rgb(255, 0, 0))
|
231
|
-
new_img
|
232
|
-
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)]
|
233
197
|
end
|
234
198
|
|
235
|
-
def
|
236
|
-
|
237
|
-
|
238
|
-
[left, top, right, bottom]
|
239
|
-
end
|
199
|
+
def preprocess_images(images, driver = self)
|
200
|
+
old_img = preprocess_image(images.first, driver)
|
201
|
+
new_img = preprocess_image(images.last, driver)
|
240
202
|
|
241
|
-
|
242
|
-
old_img.height.times do |y|
|
243
|
-
old_img.width.times do |x|
|
244
|
-
return [x, y, x, y] unless same_color?(old_img, new_img, x, y)
|
245
|
-
end
|
246
|
-
end
|
247
|
-
nil
|
203
|
+
[old_img, new_img]
|
248
204
|
end
|
249
205
|
|
250
|
-
def
|
251
|
-
|
252
|
-
bottom = @bottom
|
253
|
-
left = @left || old_img.width - 1
|
254
|
-
right = @right || 0
|
255
|
-
old_img.height.times do |y|
|
256
|
-
(0...left).find do |x|
|
257
|
-
next if same_color?(old_img, new_img, x, y)
|
258
|
-
|
259
|
-
top ||= y
|
260
|
-
bottom = y
|
261
|
-
left = x
|
262
|
-
right = x if x > right
|
263
|
-
x
|
264
|
-
end
|
265
|
-
(old_img.width - 1).step(right + 1, -1).find do |x|
|
266
|
-
unless same_color?(old_img, new_img, x, y)
|
267
|
-
bottom = y
|
268
|
-
right = x
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|
272
|
-
[left, top, right, bottom]
|
273
|
-
end
|
206
|
+
def preprocess_image(image, driver = self)
|
207
|
+
result = image
|
274
208
|
|
275
|
-
|
276
|
-
|
277
|
-
(old_img.height - 1).step(bottom + 1, -1).find do |y|
|
278
|
-
(left..right).find do |x|
|
279
|
-
bottom = y unless same_color?(old_img, new_img, x, y)
|
280
|
-
end
|
281
|
-
end
|
209
|
+
if @dimensions && driver.inscribed?(@dimensions, result)
|
210
|
+
result = driver.crop(@dimensions, result)
|
282
211
|
end
|
283
|
-
bottom
|
284
|
-
end
|
285
212
|
|
286
|
-
|
287
|
-
|
288
|
-
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)
|
289
215
|
end
|
290
216
|
|
291
|
-
|
292
|
-
|
293
|
-
if !@max_color_distance || color_distance > @max_color_distance
|
294
|
-
@max_color_distance = color_distance
|
217
|
+
if @skip_area
|
218
|
+
result = @skip_area.reduce(result) { |image, region| driver.add_black_box(image, region) }
|
295
219
|
end
|
296
|
-
color_matches = color_distance == 0 || (@color_distance_limit && @color_distance_limit > 0 &&
|
297
|
-
color_distance <= @color_distance_limit)
|
298
|
-
return color_matches if !@shift_distance_limit || @max_shift_distance == Float::INFINITY
|
299
|
-
|
300
|
-
shift_distance = (color_matches && 0) ||
|
301
|
-
shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
|
302
|
-
if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
|
303
|
-
@max_shift_distance = shift_distance
|
304
|
-
end
|
305
|
-
color_matches
|
306
|
-
end
|
307
220
|
|
308
|
-
|
309
|
-
org_color = old_img[x, y]
|
310
|
-
if shift_distance_limit
|
311
|
-
start_x = [0, x - shift_distance_limit].max
|
312
|
-
end_x = [x + shift_distance_limit, new_img.width - 1].min
|
313
|
-
xs = (start_x..end_x).to_a
|
314
|
-
start_y = [0, y - shift_distance_limit].max
|
315
|
-
end_y = [y + shift_distance_limit, new_img.height - 1].min
|
316
|
-
ys = (start_y..end_y).to_a
|
317
|
-
new_pixels = xs.product(ys)
|
318
|
-
distances = new_pixels.map do |dx, dy|
|
319
|
-
new_color = new_img[dx, dy]
|
320
|
-
ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
|
321
|
-
end
|
322
|
-
distances.min
|
323
|
-
else
|
324
|
-
ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
|
325
|
-
end
|
221
|
+
result
|
326
222
|
end
|
327
223
|
|
328
|
-
def
|
329
|
-
|
330
|
-
shift_distance = 0
|
331
|
-
loop do
|
332
|
-
bounds_breached = 0
|
333
|
-
top_row = y - shift_distance
|
334
|
-
if top_row >= 0 # top
|
335
|
-
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
336
|
-
if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
|
337
|
-
return shift_distance
|
338
|
-
end
|
339
|
-
end
|
340
|
-
else
|
341
|
-
bounds_breached += 1
|
342
|
-
end
|
343
|
-
if shift_distance > 0
|
344
|
-
if (x - shift_distance) >= 0 # left
|
345
|
-
([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
|
346
|
-
.each do |dy|
|
347
|
-
if color_matches(new_img, org_color, x - shift_distance, dy, color_distance_limit)
|
348
|
-
return shift_distance
|
349
|
-
end
|
350
|
-
end
|
351
|
-
else
|
352
|
-
bounds_breached += 1
|
353
|
-
end
|
354
|
-
if (y + shift_distance) < new_img.height # bottom
|
355
|
-
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
356
|
-
if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
|
357
|
-
return shift_distance
|
358
|
-
end
|
359
|
-
end
|
360
|
-
else
|
361
|
-
bounds_breached += 1
|
362
|
-
end
|
363
|
-
if (x + shift_distance) < new_img.width # right
|
364
|
-
([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
|
365
|
-
.each do |dy|
|
366
|
-
if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
|
367
|
-
return shift_distance
|
368
|
-
end
|
369
|
-
end
|
370
|
-
else
|
371
|
-
bounds_breached += 1
|
372
|
-
end
|
373
|
-
end
|
374
|
-
break if bounds_breached == 4
|
375
|
-
|
376
|
-
shift_distance += 1
|
377
|
-
end
|
378
|
-
Float::INFINITY
|
224
|
+
def difference_region=(region)
|
225
|
+
@left, @top, @right, @bottom = region
|
379
226
|
end
|
380
227
|
|
381
|
-
def
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
+
)
|
387
236
|
end
|
388
237
|
end
|
389
238
|
end
|