capybara-screenshot-diff 1.3.0 → 1.5.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 +138 -0
- data/.gitignore +1 -1
- data/.standard.yml +11 -0
- data/Dockerfile +60 -0
- data/README.md +87 -3
- 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 +89 -37
- 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 +21 -112
- data/.rubocop.yml +0 -62
- 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
@@ -1,34 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
lib = File.expand_path(
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require
|
5
|
+
require "capybara/screenshot/diff/version"
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
|
-
spec.name
|
9
|
-
spec.version
|
10
|
-
spec.authors
|
11
|
-
spec.email
|
12
|
-
spec.summary
|
13
|
-
spec.description
|
14
|
-
spec.homepage
|
15
|
-
spec.required_ruby_version =
|
16
|
-
spec.license =
|
17
|
-
spec.metadata[
|
8
|
+
spec.name = "capybara-screenshot-diff"
|
9
|
+
spec.version = Capybara::Screenshot::Diff::VERSION
|
10
|
+
spec.authors = ["Uwe Kubosch"]
|
11
|
+
spec.email = ["uwe@kubosch.no"]
|
12
|
+
spec.summary = "Track your GUI changes with diff assertions"
|
13
|
+
spec.description = "Save screen shots and track changes with graphical diff"
|
14
|
+
spec.homepage = "https://github.com/donv/capybara-screenshot-diff"
|
15
|
+
spec.required_ruby_version = ">= 2.5.0"
|
16
|
+
spec.license = "MIT"
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org/"
|
18
18
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
-
spec.bindir
|
20
|
-
spec.executables
|
21
|
-
spec.require_paths = [
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
-
spec.add_runtime_dependency
|
24
|
-
spec.add_runtime_dependency
|
25
|
-
spec.add_runtime_dependency
|
26
|
-
|
27
|
-
spec.add_development_dependency 'bundler', '~> 1.11'
|
28
|
-
spec.add_development_dependency 'minitest', '~> 5.0'
|
29
|
-
spec.add_development_dependency 'minitest-reporters'
|
30
|
-
spec.add_development_dependency 'rake'
|
31
|
-
spec.add_development_dependency 'rubocop', '~> 0.54'
|
32
|
-
spec.add_development_dependency 'rubocop-performance', '~> 0.0'
|
33
|
-
spec.add_development_dependency 'simplecov', '~> 0.11'
|
23
|
+
spec.add_runtime_dependency "actionpack", ">= 4.2", "< 7"
|
24
|
+
spec.add_runtime_dependency "capybara", ">= 2", "< 4"
|
25
|
+
spec.add_runtime_dependency "chunky_png", "~> 1.3"
|
34
26
|
end
|
data/gemfiles/rails42.gemfile
CHANGED
data/gemfiles/rails50.gemfile
CHANGED
data/gemfiles/rails51.gemfile
CHANGED
data/gemfiles/rails52.gemfile
CHANGED
data/gems.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in capybara-screenshot-diff.gemspec
|
6
|
+
gemspec path: __dir__
|
7
|
+
|
8
|
+
gem "rake"
|
9
|
+
|
10
|
+
# Image processing libraries
|
11
|
+
gem "oily_png", platform: :ruby
|
12
|
+
gem "ruby-vips", require: false
|
13
|
+
|
14
|
+
# Test
|
15
|
+
gem "minitest", require: false
|
16
|
+
gem "minitest-stub-const", require: false
|
17
|
+
gem "simplecov", require: false
|
18
|
+
|
19
|
+
# Capybara Server
|
20
|
+
gem "puma", require: false
|
21
|
+
|
22
|
+
# Capybara Drivers
|
23
|
+
gem "cuprite", require: false
|
24
|
+
gem "selenium-webdriver", require: false
|
25
|
+
gem "webdrivers", require: false
|
26
|
+
|
27
|
+
group :tools do
|
28
|
+
gem "standard", require: false
|
29
|
+
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "capybara/dsl"
|
4
|
+
require "capybara/screenshot/diff/version"
|
5
|
+
require "capybara/screenshot/diff/drivers/utils"
|
6
|
+
require "capybara/screenshot/diff/image_compare"
|
7
|
+
require "capybara/screenshot/diff/test_methods"
|
7
8
|
|
8
9
|
module Capybara
|
9
10
|
module Screenshot
|
@@ -13,12 +14,17 @@ module Screenshot
|
|
13
14
|
mattr_accessor :blur_active_element
|
14
15
|
mattr_accessor :enabled
|
15
16
|
mattr_accessor :hide_caret
|
16
|
-
|
17
|
+
mattr_reader(:root) { (defined?(Rails.root) && Rails.root) || Pathname(".").expand_path }
|
17
18
|
mattr_accessor :stability_time_limit
|
18
19
|
mattr_accessor :window_size
|
19
|
-
mattr_accessor(:save_path) {
|
20
|
+
mattr_accessor(:save_path) { "doc/screenshots" }
|
21
|
+
mattr_accessor(:use_lfs)
|
20
22
|
|
21
23
|
class << self
|
24
|
+
def root=(path)
|
25
|
+
@@root = Pathname(path).expand_path
|
26
|
+
end
|
27
|
+
|
22
28
|
def active?
|
23
29
|
enabled || (enabled.nil? && Diff.enabled)
|
24
30
|
end
|
@@ -31,7 +37,7 @@ def screenshot_area
|
|
31
37
|
end
|
32
38
|
|
33
39
|
def screenshot_area_abs
|
34
|
-
|
40
|
+
root / screenshot_area
|
35
41
|
end
|
36
42
|
end
|
37
43
|
|
@@ -45,20 +51,24 @@ module Diff
|
|
45
51
|
mattr_accessor(:enabled) { true }
|
46
52
|
mattr_accessor :shift_distance_limit
|
47
53
|
mattr_accessor :skip_area
|
54
|
+
mattr_accessor(:driver) { :auto }
|
55
|
+
mattr_accessor(:tolerance) { 0.001 }
|
56
|
+
|
57
|
+
AVAILABLE_DRIVERS = Utils.detect_available_drivers.freeze
|
48
58
|
|
49
|
-
def self.included(
|
50
|
-
|
51
|
-
|
59
|
+
def self.included(klass)
|
60
|
+
klass.include TestMethods
|
61
|
+
klass.setup do
|
52
62
|
if Capybara::Screenshot.window_size
|
53
|
-
if
|
54
|
-
page.driver.browser.manage.window.resize_to(*Capybara::Screenshot.window_size)
|
55
|
-
elsif poltergeist?
|
63
|
+
if page.driver.respond_to?(:resize)
|
56
64
|
page.driver.resize(*Capybara::Screenshot.window_size)
|
65
|
+
elsif selenium?
|
66
|
+
page.driver.browser.manage.window.resize_to(*Capybara::Screenshot.window_size)
|
57
67
|
end
|
58
68
|
end
|
59
69
|
end
|
60
70
|
|
61
|
-
|
71
|
+
klass.teardown do
|
62
72
|
if Capybara::Screenshot::Diff.enabled && @test_screenshots
|
63
73
|
test_screenshot_errors = @test_screenshots
|
64
74
|
.map { |caller, name, compare| assert_image_not_changed(caller, name, compare) }
|
@@ -0,0 +1,355 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "chunky_png"
|
4
|
+
|
5
|
+
module Capybara
|
6
|
+
module Screenshot
|
7
|
+
module Diff
|
8
|
+
# Compare two images and determine if they are equal, different, or within some comparison
|
9
|
+
# range considering color values and difference area size.
|
10
|
+
module Drivers
|
11
|
+
class ChunkyPNGDriver
|
12
|
+
include ChunkyPNG::Color
|
13
|
+
|
14
|
+
attr_reader :new_file_name, :old_file_name
|
15
|
+
|
16
|
+
def initialize(new_file_name, old_file_name = nil, **options)
|
17
|
+
@new_file_name = new_file_name
|
18
|
+
@old_file_name = old_file_name || "#{new_file_name}~"
|
19
|
+
|
20
|
+
@color_distance_limit = options[:color_distance_limit]
|
21
|
+
@shift_distance_limit = options[:shift_distance_limit]
|
22
|
+
@skip_area = options[:skip_area]
|
23
|
+
|
24
|
+
reset
|
25
|
+
end
|
26
|
+
|
27
|
+
# Resets the calculated data about the comparison with regard to the "new_image".
|
28
|
+
# Data about the original image is kept.
|
29
|
+
def reset
|
30
|
+
@max_color_distance = @color_distance_limit ? 0 : nil
|
31
|
+
@max_shift_distance = @shift_distance_limit ? 0 : nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_images(old_file_name, new_file_name)
|
35
|
+
old_bytes, new_bytes = load_image_files(old_file_name, new_file_name)
|
36
|
+
|
37
|
+
_load_images(old_bytes, new_bytes)
|
38
|
+
end
|
39
|
+
|
40
|
+
def filter_image_with_median(_image)
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_black_box(image, _region)
|
45
|
+
image
|
46
|
+
end
|
47
|
+
|
48
|
+
def difference_level(_diff_mask, old_img, region)
|
49
|
+
size(region).to_f / image_area_size(old_img)
|
50
|
+
end
|
51
|
+
|
52
|
+
def image_area_size(old_img)
|
53
|
+
width_for(old_img) * height_for(old_img)
|
54
|
+
end
|
55
|
+
|
56
|
+
def shift_distance_equal?
|
57
|
+
# Stub
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def shift_distance_different?
|
62
|
+
# Stub
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_difference_region(new_image, old_image, color_distance_limit, shift_distance_limit, area_size_limit, fast_fail: false)
|
67
|
+
return nil, nil if new_image.pixels == old_image.pixels
|
68
|
+
|
69
|
+
if fast_fail && !(color_distance_limit || shift_distance_limit || area_size_limit)
|
70
|
+
return [0, 0, width_for(new_image), height_for(new_image)], nil
|
71
|
+
end
|
72
|
+
|
73
|
+
region = find_top(old_image, new_image)
|
74
|
+
region = if region.nil? || region[1].nil?
|
75
|
+
nil
|
76
|
+
else
|
77
|
+
find_diff_rectangle(old_image, new_image, region)
|
78
|
+
end
|
79
|
+
|
80
|
+
[region, nil]
|
81
|
+
end
|
82
|
+
|
83
|
+
def height_for(image)
|
84
|
+
image.height
|
85
|
+
end
|
86
|
+
|
87
|
+
def width_for(image)
|
88
|
+
image.width
|
89
|
+
end
|
90
|
+
|
91
|
+
def size(region)
|
92
|
+
return 0 unless region
|
93
|
+
|
94
|
+
(region[2] - region[0] + 1) * (region[3] - region[1] + 1)
|
95
|
+
end
|
96
|
+
|
97
|
+
def max_color_distance
|
98
|
+
calculate_metrics unless @max_color_distance
|
99
|
+
@max_color_distance
|
100
|
+
end
|
101
|
+
|
102
|
+
def max_shift_distance
|
103
|
+
calculate_metrics unless @max_shift_distance || !@shift_distance_limit
|
104
|
+
@max_shift_distance
|
105
|
+
end
|
106
|
+
|
107
|
+
def adds_error_details_to(log)
|
108
|
+
max_color_distance = self.max_color_distance.ceil(1)
|
109
|
+
max_shift_distance = self.max_shift_distance
|
110
|
+
|
111
|
+
log[:max_color_distance] = max_color_distance
|
112
|
+
log.merge!(max_shift_distance: max_shift_distance) if max_shift_distance
|
113
|
+
end
|
114
|
+
|
115
|
+
def crop(dimensions, i)
|
116
|
+
i.crop(0, 0, *dimensions)
|
117
|
+
end
|
118
|
+
|
119
|
+
def from_file(filename)
|
120
|
+
ChunkyPNG::Image.from_file(filename)
|
121
|
+
end
|
122
|
+
|
123
|
+
# private
|
124
|
+
|
125
|
+
def calculate_metrics
|
126
|
+
old_file, new_file = load_image_files(@old_file_name, @new_file_name)
|
127
|
+
|
128
|
+
if old_file == new_file
|
129
|
+
@max_color_distance = 0
|
130
|
+
@max_shift_distance = 0
|
131
|
+
return
|
132
|
+
end
|
133
|
+
|
134
|
+
old_image, new_image = _load_images(old_file, new_file)
|
135
|
+
calculate_max_color_distance(new_image, old_image)
|
136
|
+
calculate_max_shift_limit(new_image, old_image)
|
137
|
+
end
|
138
|
+
|
139
|
+
def calculate_max_color_distance(new_image, old_image)
|
140
|
+
pixel_pairs = old_image.pixels.zip(new_image.pixels)
|
141
|
+
@max_color_distance = pixel_pairs.inject(0) { |max, (p1, p2)|
|
142
|
+
next max unless p1 && p2
|
143
|
+
|
144
|
+
d = ChunkyPNG::Color.euclidean_distance_rgba(p1, p2)
|
145
|
+
[max, d].max
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def calculate_max_shift_limit(new_img, old_img)
|
150
|
+
(0...new_img.width).each do |x|
|
151
|
+
(0...new_img.height).each do |y|
|
152
|
+
shift_distance =
|
153
|
+
shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
|
154
|
+
if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
|
155
|
+
@max_shift_distance = shift_distance
|
156
|
+
return if @max_shift_distance == Float::INFINITY # rubocop: disable Lint/NonLocalExitFromIterator
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def save_image_to(image, filename)
|
163
|
+
image.save(filename)
|
164
|
+
end
|
165
|
+
|
166
|
+
def resize_image_to(image, new_width, new_height)
|
167
|
+
image.resample_bilinear(new_width, new_height)
|
168
|
+
end
|
169
|
+
|
170
|
+
def load_image_files(old_file_name, file_name)
|
171
|
+
old_file = File.binread(old_file_name)
|
172
|
+
new_file = File.binread(file_name)
|
173
|
+
[old_file, new_file]
|
174
|
+
end
|
175
|
+
|
176
|
+
def dimension_changed?(old_image, new_image)
|
177
|
+
return unless old_image.dimension != new_image.dimension
|
178
|
+
|
179
|
+
change_msg = [old_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
|
180
|
+
warn "Image size has changed for #{@new_file_name}: #{change_msg}"
|
181
|
+
true
|
182
|
+
end
|
183
|
+
|
184
|
+
def draw_rectangles(images, (left, top, right, bottom), (r, g, b))
|
185
|
+
images.map do |image|
|
186
|
+
new_img = image.dup
|
187
|
+
new_img.rect(left - 1, top - 1, right + 1, bottom + 1, ChunkyPNG::Color.rgb(r, g, b))
|
188
|
+
new_img
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
def find_diff_rectangle(org_img, new_img, region)
|
195
|
+
left, top, right, bottom = find_left_right_and_top(org_img, new_img, region)
|
196
|
+
bottom = find_bottom(org_img, new_img, left, right, bottom)
|
197
|
+
[left, top, right, bottom]
|
198
|
+
end
|
199
|
+
|
200
|
+
def find_top(old_img, new_img)
|
201
|
+
old_img.height.times do |y|
|
202
|
+
old_img.width.times do |x|
|
203
|
+
return [x, y, x, y] unless same_color?(old_img, new_img, x, y)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
nil
|
207
|
+
end
|
208
|
+
|
209
|
+
def find_left_right_and_top(old_img, new_img, region)
|
210
|
+
left = region[0] || old_img.width - 1
|
211
|
+
top = region[1]
|
212
|
+
bottom = region[2]
|
213
|
+
right = region[3] || 0
|
214
|
+
old_img.height.times do |y|
|
215
|
+
(0...left).find do |x|
|
216
|
+
next if same_color?(old_img, new_img, x, y)
|
217
|
+
|
218
|
+
top ||= y
|
219
|
+
bottom = y
|
220
|
+
left = x
|
221
|
+
right = x if x > right
|
222
|
+
x
|
223
|
+
end
|
224
|
+
(old_img.width - 1).step(right + 1, -1).find do |x|
|
225
|
+
unless same_color?(old_img, new_img, x, y)
|
226
|
+
bottom = y
|
227
|
+
right = x
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
[left, top, right, bottom]
|
232
|
+
end
|
233
|
+
|
234
|
+
def find_bottom(old_img, new_img, left, right, bottom)
|
235
|
+
if bottom
|
236
|
+
(old_img.height - 1).step(bottom + 1, -1).find do |y|
|
237
|
+
(left..right).find do |x|
|
238
|
+
bottom = y unless same_color?(old_img, new_img, x, y)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
bottom
|
243
|
+
end
|
244
|
+
|
245
|
+
def same_color?(old_img, new_img, x, y)
|
246
|
+
@skip_area&.each do |skip_start_x, skip_start_y, skip_end_x, skip_end_y|
|
247
|
+
return true if skip_start_x <= x && x <= skip_end_x && skip_start_y <= y && y <= skip_end_y
|
248
|
+
end
|
249
|
+
|
250
|
+
color_distance =
|
251
|
+
color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit)
|
252
|
+
if !@max_color_distance || color_distance > @max_color_distance
|
253
|
+
@max_color_distance = color_distance
|
254
|
+
end
|
255
|
+
color_matches = color_distance == 0 || (@color_distance_limit && @color_distance_limit > 0 &&
|
256
|
+
color_distance <= @color_distance_limit)
|
257
|
+
return color_matches if !@shift_distance_limit || @max_shift_distance == Float::INFINITY
|
258
|
+
|
259
|
+
shift_distance = (color_matches && 0) ||
|
260
|
+
shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
|
261
|
+
if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
|
262
|
+
@max_shift_distance = shift_distance
|
263
|
+
end
|
264
|
+
color_matches
|
265
|
+
end
|
266
|
+
|
267
|
+
def color_distance_at(new_img, old_img, x, y, shift_distance_limit:)
|
268
|
+
org_color = old_img[x, y]
|
269
|
+
if shift_distance_limit
|
270
|
+
start_x = [0, x - shift_distance_limit].max
|
271
|
+
end_x = [x + shift_distance_limit, new_img.width - 1].min
|
272
|
+
xs = (start_x..end_x).to_a
|
273
|
+
start_y = [0, y - shift_distance_limit].max
|
274
|
+
end_y = [y + shift_distance_limit, new_img.height - 1].min
|
275
|
+
ys = (start_y..end_y).to_a
|
276
|
+
new_pixels = xs.product(ys)
|
277
|
+
distances = new_pixels.map { |dx, dy|
|
278
|
+
new_color = new_img[dx, dy]
|
279
|
+
ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
|
280
|
+
}
|
281
|
+
distances.min
|
282
|
+
else
|
283
|
+
ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def shift_distance_at(new_img, old_img, x, y, color_distance_limit:)
|
288
|
+
org_color = old_img[x, y]
|
289
|
+
shift_distance = 0
|
290
|
+
loop do
|
291
|
+
bounds_breached = 0
|
292
|
+
top_row = y - shift_distance
|
293
|
+
if top_row >= 0 # top
|
294
|
+
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
295
|
+
if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
|
296
|
+
return shift_distance
|
297
|
+
end
|
298
|
+
end
|
299
|
+
else
|
300
|
+
bounds_breached += 1
|
301
|
+
end
|
302
|
+
if shift_distance > 0
|
303
|
+
if (x - shift_distance) >= 0 # left
|
304
|
+
([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
|
305
|
+
.each do |dy|
|
306
|
+
if color_matches(new_img, org_color, x - shift_distance, dy, color_distance_limit)
|
307
|
+
return shift_distance
|
308
|
+
end
|
309
|
+
end
|
310
|
+
else
|
311
|
+
bounds_breached += 1
|
312
|
+
end
|
313
|
+
if (y + shift_distance) < new_img.height # bottom
|
314
|
+
([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
|
315
|
+
if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
|
316
|
+
return shift_distance
|
317
|
+
end
|
318
|
+
end
|
319
|
+
else
|
320
|
+
bounds_breached += 1
|
321
|
+
end
|
322
|
+
if (x + shift_distance) < new_img.width # right
|
323
|
+
([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
|
324
|
+
.each do |dy|
|
325
|
+
if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
|
326
|
+
return shift_distance
|
327
|
+
end
|
328
|
+
end
|
329
|
+
else
|
330
|
+
bounds_breached += 1
|
331
|
+
end
|
332
|
+
end
|
333
|
+
break if bounds_breached == 4
|
334
|
+
|
335
|
+
shift_distance += 1
|
336
|
+
end
|
337
|
+
Float::INFINITY
|
338
|
+
end
|
339
|
+
|
340
|
+
def color_matches(new_img, org_color, x, y, color_distance_limit)
|
341
|
+
new_color = new_img[x, y]
|
342
|
+
return new_color == org_color unless color_distance_limit
|
343
|
+
|
344
|
+
color_distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
|
345
|
+
color_distance <= color_distance_limit
|
346
|
+
end
|
347
|
+
|
348
|
+
def _load_images(old_file, new_file)
|
349
|
+
[ChunkyPNG::Image.from_blob(old_file), ChunkyPNG::Image.from_blob(new_file)]
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|