capybara-screenshot-diff 1.3.1 → 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 +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
|