watir_visual_diff 0.0.1

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +48 -0
  5. data/.travis.yml +14 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +116 -0
  9. data/Rakefile +10 -0
  10. data/bin/console +7 -0
  11. data/bin/setup +6 -0
  12. data/examples/performance.rb +42 -0
  13. data/lib/watir_visual_diff/color_methods.rb +23 -0
  14. data/lib/watir_visual_diff/image.rb +60 -0
  15. data/lib/watir_visual_diff/matcher.rb +51 -0
  16. data/lib/watir_visual_diff/modes/base.rb +63 -0
  17. data/lib/watir_visual_diff/modes/delta.rb +54 -0
  18. data/lib/watir_visual_diff/modes/grayscale.rb +49 -0
  19. data/lib/watir_visual_diff/modes/rgb.rb +34 -0
  20. data/lib/watir_visual_diff/modes.rb +7 -0
  21. data/lib/watir_visual_diff/rectangle.rb +31 -0
  22. data/lib/watir_visual_diff/result.rb +23 -0
  23. data/lib/watir_visual_diff/version.rb +3 -0
  24. data/lib/watir_visual_diff.rb +14 -0
  25. data/spec/fixtures/a.png +0 -0
  26. data/spec/fixtures/a1.png +0 -0
  27. data/spec/fixtures/b.png +0 -0
  28. data/spec/fixtures/darker.png +0 -0
  29. data/spec/fixtures/delta_diff.png +0 -0
  30. data/spec/fixtures/exclude.png +0 -0
  31. data/spec/fixtures/grayscale_diff.png +0 -0
  32. data/spec/fixtures/include.png +0 -0
  33. data/spec/fixtures/rgb_diff.png +0 -0
  34. data/spec/fixtures/small.png +0 -0
  35. data/spec/fixtures/very_small.png +0 -0
  36. data/spec/image_spec.rb +11 -0
  37. data/spec/imatcher_spec.rb +7 -0
  38. data/spec/integrations/delta_spec.rb +51 -0
  39. data/spec/integrations/grayscale_spec.rb +71 -0
  40. data/spec/integrations/rgb_spec.rb +51 -0
  41. data/spec/matcher_spec.rb +77 -0
  42. data/spec/rectangle_spec.rb +37 -0
  43. data/spec/spec_helper.rb +14 -0
  44. data/watir_visual_diff.gemspec +21 -0
  45. metadata +129 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4b59144d0fc2d9d5c3267367ab8c212e10cc2a57
4
+ data.tar.gz: f8b9b36c9bef41891938a8084d71c1d6bd3e8ce8
5
+ SHA512:
6
+ metadata.gz: 60d535c8e4e962fc48846c9b0212059bd2dc73a5eb7ae312ed6969a7f17bc4537a686360ffccbe47535c0fb5d733d13529f21c2c7c28768fecfad14e65d905cf
7
+ data.tar.gz: af3dc2a60f7db4226af17676620595f3b31d9b2f4c911b141fd2d94c4369484fa6e088784e40a782fe7ef7078449f5db1e8095f0f8f61f06ff728991eccfab6c
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ # Numerous always-ignore extensions
2
+ *.diff
3
+ *.err
4
+ *.orig
5
+ *.log
6
+ *.rej
7
+ *.swo
8
+ *.swp
9
+ *.vi
10
+ *~
11
+ *.sass-cache
12
+ *.iml
13
+ .idea/
14
+
15
+ # Sublime
16
+ *.sublime-project
17
+ *.sublime-workspace
18
+
19
+ # OS or Editor folders
20
+ .DS_Store
21
+ .cache
22
+ .project
23
+ .settings
24
+ .tmproj
25
+ Thumbs.db
26
+ *.gem
27
+
28
+ /.bundle/
29
+ /.yardoc
30
+ /Gemfile.lock
31
+ /_yardoc/
32
+ /coverage/
33
+ /doc/
34
+ /pkg/
35
+ /spec/reports/
36
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,48 @@
1
+ AllCops:
2
+ # Include gemspec and Rakefile
3
+ Include:
4
+ - 'lib/**/*.rb'
5
+ - 'lib/**/*.rake'
6
+ - 'spec/**/*.rb'
7
+ Exclude:
8
+ - 'bin/**/*'
9
+ - 'spec/dummy/**/*'
10
+ Rails:
11
+ Enabled: false
12
+ DisplayCopNames: true
13
+ StyleGuideCopsOnly: false
14
+
15
+ Style/AccessorMethodName:
16
+ Enabled: false
17
+
18
+ Style/TrivialAccessors:
19
+ Enabled: false
20
+
21
+ Style/Documentation:
22
+ Exclude:
23
+ - 'spec/**/*.rb'
24
+
25
+ Style/StringLiterals:
26
+ Enabled: false
27
+
28
+ Style/SpaceInsideStringInterpolation:
29
+ EnforcedStyle: no_space
30
+
31
+ Style/BlockDelimiters:
32
+ Exclude:
33
+ - 'spec/**/*.rb'
34
+
35
+ Style/ParallelAssignment:
36
+ Enabled: false
37
+
38
+ Lint/AmbiguousRegexpLiteral:
39
+ Enabled: false
40
+
41
+ Metrics/MethodLength:
42
+ Exclude:
43
+ - 'spec/**/*.rb'
44
+
45
+ Metrics/LineLength:
46
+ max: 100
47
+ Exclude:
48
+ - 'spec/**/*.rb'
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.2
5
+ - ruby-head
6
+ - jruby-head
7
+
8
+ notifications:
9
+ email: false
10
+
11
+ before_install:
12
+ - gem install bundler
13
+
14
+ script: bundle exec rake
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "rake", "~> 10.0"
4
+ gem "rspec", "~> 3.0"
5
+ gem "oily_png", "~> 1.2.1"
6
+ local_gemfile = 'Gemfile.local'
7
+
8
+ if File.exist?(local_gemfile)
9
+ eval(File.read(local_gemfile)) # rubocop:disable Lint/Eval
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Justin Commu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ [![Gem Version](https://badge.fury.io/rb/watir_visual_diff.svg)](https://rubygems.org/gems/watir_visual_diff) [![Build Status](https://travis-ci.org/teachbase/watir_visual_diff.svg?branch=master)](https://travis-ci.org/teachbase/watir_visual_diff)
2
+
3
+ # WatirVisualDiff
4
+
5
+ Compare PNG images in pure Ruby (uses [ChunkyPNG](https://github.com/wvanbergen/chunky_png)) using different algorithms.
6
+ This is an utility library for image regression testing.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'watir_visual_diff'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install watir_visual_diff
23
+
24
+ Additionally, you may want to install [oily_png](https://github.com/wvanbergen/oily_png) to improve performance when using MRI. Just install it globally or add to your Gemfile.
25
+
26
+ ## Modes
27
+
28
+ WatirVisualDiff supports different ways (_modes_) of comparing images.
29
+
30
+ Source images used in examples:
31
+
32
+ <img src="https://raw.githubusercontent.com/teachbase/watir_visual_diff/master/spec/fixtures/a.png" width="300" />
33
+ <img src="https://raw.githubusercontent.com/teachbase/watir_visual_diff/master/spec/fixtures/b.png" width="300" />
34
+
35
+ ### Base (RGB) mode
36
+
37
+ Compare pixels by values, resulting score is a ratio of unequal pixels.
38
+ Resulting diff represents per-channel difference.
39
+
40
+ <img src="https://raw.githubusercontent.com/teachbase/watir_visual_diff/master/spec/fixtures/rgb_diff.png" width="300" />
41
+
42
+ ### Grayscale mode
43
+
44
+ Compare pixels as grayscale (by brightness and alpha), resulting score is a ratio of unequal pixels (with respect to provided tolerance).
45
+
46
+ Resulting diff contains grayscale version of the first image with different pixels highlighted in red and red bounding box.
47
+
48
+ <img src="https://raw.githubusercontent.com/teachbase/watir_visual_diff/master/spec/fixtures/grayscale_diff.png" width="300" />
49
+
50
+ ### Delta
51
+
52
+ Compare pixels using [Delta E](https://en.wikipedia.org/wiki/Color_difference) distance.
53
+ Resulting diff contains grayscale version of the first image with different pixels highlighted in red (with respect to diff score).
54
+
55
+ <img src="https://raw.githubusercontent.com/teachbase/watir_visual_diff/master/spec/fixtures/delta_diff.png" width="300" />
56
+
57
+ ## Usage
58
+
59
+ ```ruby
60
+ # create new matcher with default threshold equals to 0
61
+ # and base (RGB) mode
62
+ cmp = WatirVisualDiff::Matcher.new
63
+ cmp.mode #=> WatirVisualDiff::Modes::RGB
64
+
65
+ # create matcher with specific threshold
66
+ cmp = WatirVisualDiff::Matcher.new threshold: 0.05
67
+ cmp.threshold #=> 0.05
68
+
69
+ # create zero-tolerance grayscale matcher
70
+ cmp = WatirVisualDiff::Matcher.new mode: :grayscale, tolerance: 0
71
+ cmp.mode #=> WatirVisualDiff::Modes::Grayscale
72
+
73
+ res = cmp.compare(path_1, path_2)
74
+ res #=> WatirVisualDiff::Result
75
+
76
+ res.match? #=> true
77
+
78
+ res.score #=> 0.0
79
+
80
+ # Return diff image object
81
+ res.difference_image #=> WatirVisualDiff::Image
82
+
83
+ res.difference_image.save(new_path)
84
+
85
+ # without explicit matcher
86
+ res = WatirVisualDiff.compare(path_1, path_2, options)
87
+
88
+ # equals to
89
+ res = WatirVisualDiff::Matcher.new(options).compare(path_1, path_2)
90
+
91
+ ```
92
+
93
+ ## Excluding rectangle
94
+
95
+ <img src="https://raw.githubusercontent.com/teachbase/watir_visual_diff/master/spec/fixtures/a.png" width="300" />
96
+ <img src="https://raw.githubusercontent.com/teachbase/watir_visual_diff/master/spec/fixtures/a1.png" width="300" />
97
+
98
+ You can exclude rectangle from comparing by passing `:exclude_rect` to `compare`.
99
+ E.g., if `path_1` and `path_2` contain images above
100
+ ```ruby
101
+ WatirVisualDiff.compare(path_1, path_2, exclude_rect: [200, 150, 275, 200]).match? # => true
102
+ ```
103
+ `[200, 150, 275, 200]` is array of two vertices of rectangle -- (200, 150) is left-top vertex and (275, 200) is right-bottom.
104
+
105
+ ## Including rectangle
106
+
107
+ You can set bounds of comparing by passing `:include_rect` to `compare` with array similar to previous example
108
+
109
+ ## Contributing
110
+
111
+ Bug reports and pull requests are welcome on GitHub at https://github.com/teachbase/watir_visual_diff.
112
+
113
+ ## License
114
+
115
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
116
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ task :console do
9
+ sh 'pry -r ./lib/watir_visual_diff.rb'
10
+ end
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "watir_visual_diff"
5
+
6
+ require "pry"
7
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,42 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'benchmark/ips'
3
+ require 'watir_visual_diff'
4
+
5
+ a = WatirVisualDiff::Image.from_file(File.expand_path('../../spec/fixtures/a.png', __FILE__))
6
+ b = WatirVisualDiff::Image.from_file(File.expand_path('../../spec/fixtures/a.png', __FILE__))
7
+
8
+ rgb = WatirVisualDiff::Matcher.new
9
+ grayscale = WatirVisualDiff::Matcher.new mode: :grayscale
10
+ delta = WatirVisualDiff::Matcher.new mode: :delta
11
+
12
+ Benchmark.ips do |x|
13
+ x.report 'RGB' do
14
+ rgb.compare(a, b)
15
+ end
16
+
17
+ x.report 'Grayscale' do
18
+ grayscale.compare(a, b)
19
+ end
20
+
21
+ x.report 'Delta E' do
22
+ delta.compare(a, b)
23
+ end
24
+
25
+ x.compare!
26
+ end
27
+
28
+ Benchmark.ips do |x|
29
+ x.report 'RGB' do
30
+ rgb.compare(a, b).difference_image
31
+ end
32
+
33
+ x.report 'Grayscale' do
34
+ grayscale.compare(a, b).difference_image
35
+ end
36
+
37
+ x.report 'Delta E' do
38
+ delta.compare(a, b).difference_image
39
+ end
40
+
41
+ x.compare!
42
+ end
@@ -0,0 +1,23 @@
1
+ require "oily_png"
2
+
3
+ module WatirVisualDiff
4
+ module ColorMethods # :nodoc:
5
+ include ChunkyPNG::Color
6
+
7
+ def brightness(a)
8
+ 0.3 * r(a) + 0.59 * g(a) + 0.11 * b(a)
9
+ end
10
+
11
+ def red
12
+ rgb(255, 0, 0)
13
+ end
14
+
15
+ def green
16
+ rgb(0, 255, 0)
17
+ end
18
+
19
+ def blue
20
+ rgb(0, 0, 255)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,60 @@
1
+ require 'watir_visual_diff/color_methods'
2
+
3
+ module WatirVisualDiff
4
+ # Extend ChunkyPNG::Image with some methods.
5
+ class Image < ChunkyPNG::Image
6
+ include ColorMethods
7
+
8
+ def each_pixel
9
+ height.times do |y|
10
+ row(y).each_with_index do |pixel, x|
11
+ yield(pixel, x, y)
12
+ end
13
+ end
14
+ end
15
+
16
+ def compare_each_pixel(image, area: nil)
17
+ area = bounding_rect if area.nil?
18
+ (area.top..area.bot).each do |y|
19
+ range = (area.left..area.right)
20
+ next if image.row(y).slice(range) == row(y).slice(range)
21
+ (area.left..area.right).each do |x|
22
+ yield(self[x, y], image[x, y], x, y)
23
+ end
24
+ end
25
+ end
26
+
27
+ def to_grayscale
28
+ each_pixel do |pixel, x, y|
29
+ self[x, y] = grayscale(brightness(pixel).round)
30
+ end
31
+ self
32
+ end
33
+
34
+ def with_alpha(value)
35
+ each_pixel do |pixel, x, y|
36
+ self[x, y] = rgba(r(pixel), g(pixel), b(pixel), value)
37
+ end
38
+ self
39
+ end
40
+
41
+ def sizes_match?(image)
42
+ [width, height] == [image.width, image.height]
43
+ end
44
+
45
+ def inspect
46
+ "Image:#{object_id}<#{width}x#{height}>"
47
+ end
48
+
49
+ def highlight_rectangle(rect, color = :red)
50
+ fail ArgumentError, "Undefined color: #{color}" unless respond_to?(color)
51
+ return self if rect.nil?
52
+ rect(*rect.bounds, send(color))
53
+ self
54
+ end
55
+
56
+ def bounding_rect
57
+ Rectangle.new(0, 0, width - 1, height - 1)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ module WatirVisualDiff
2
+ # Matcher contains information about compare mode
3
+ class Matcher
4
+ require 'watir_visual_diff/image'
5
+ require 'watir_visual_diff/result'
6
+ require 'watir_visual_diff/modes'
7
+
8
+ MODES = {
9
+ rgb: 'RGB',
10
+ delta: 'Delta',
11
+ grayscale: 'Grayscale'
12
+ }.freeze
13
+
14
+ attr_reader :threshold, :mode
15
+
16
+ def initialize(options = {})
17
+ mode_type = options.delete(:mode) || :rgb
18
+ fail ArgumentError, "Undefined mode: #{ mode_type }" unless MODES.keys.include?(mode_type)
19
+ @mode = Modes.const_get(MODES[mode_type]).new(options)
20
+ end
21
+
22
+ def compare(a, b)
23
+ a = Image.from_file(a) unless a.is_a?(Image)
24
+ b = Image.from_file(b) unless b.is_a?(Image)
25
+
26
+ fail SizesMismatchError,
27
+ "Size mismatch: first image size: " \
28
+ "#{a.width}x#{a.height}, " \
29
+ "second image size: " \
30
+ "#{b.width}x#{b.height}" unless a.sizes_match?(b)
31
+
32
+ image_area = Rectangle.new(0, 0, a.width - 1, a.height - 1)
33
+
34
+ unless mode.exclude_rect.nil?
35
+ fail ArgumentError,
36
+ "Bounds must be in image" unless image_area.contains?(mode.exclude_rect)
37
+ end
38
+
39
+ unless mode.include_rect.nil?
40
+ fail ArgumentError,
41
+ "Bounds must be in image" unless image_area.contains?(mode.include_rect)
42
+ unless mode.exclude_rect.nil?
43
+ fail ArgumentError,
44
+ "Included area must contain excluded" unless mode.include_rect.contains?(mode.exclude_rect)
45
+ end
46
+ end
47
+
48
+ mode.compare(a, b)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ module WatirVisualDiff
2
+ module Modes
3
+ class Base # :nodoc:
4
+ require 'watir_visual_diff/rectangle'
5
+ include ColorMethods
6
+
7
+ attr_reader :result, :threshold, :bounds, :exclude_rect, :include_rect
8
+
9
+ def initialize(threshold: 0.0, exclude_rect: nil, include_rect: nil)
10
+ @include_rect = Rectangle.new(*include_rect) unless include_rect.nil?
11
+ @exclude_rect = Rectangle.new(*exclude_rect) unless exclude_rect.nil?
12
+ @threshold = threshold
13
+ @result = Result.new(self, threshold)
14
+ end
15
+
16
+ def compare(a, b)
17
+ result.image = a
18
+ @include_rect ||= a.bounding_rect
19
+ @bounds = Rectangle.new(*include_rect.bounds)
20
+
21
+ b.compare_each_pixel(a, area: include_rect) do |b_pixel, a_pixel, x, y|
22
+ next if pixels_equal?(b_pixel, a_pixel)
23
+ next if !exclude_rect.nil? && exclude_rect.contains_point?(x, y)
24
+ update_result(b_pixel, a_pixel, x, y)
25
+ end
26
+
27
+ result.score = score
28
+ result
29
+ end
30
+
31
+ def diff(bg, diff)
32
+ diff_image = background(bg).highlight_rectangle(exclude_rect, :blue)
33
+ diff.each do |pixels_pair|
34
+ pixels_diff(diff_image, *pixels_pair)
35
+ end
36
+ create_diff_image(bg, diff_image).
37
+ highlight_rectangle(bounds).
38
+ highlight_rectangle(include_rect, :green)
39
+ end
40
+
41
+ def score
42
+ result.diff.length.to_f / area
43
+ end
44
+
45
+ def update_result(*_args, x, y)
46
+ update_bounds(x, y)
47
+ end
48
+
49
+ def update_bounds(x, y)
50
+ bounds.left = [x, bounds.left].max
51
+ bounds.top = [y, bounds.top].max
52
+ bounds.right = [x, bounds.right].min
53
+ bounds.bot = [y, bounds.bot].min
54
+ end
55
+
56
+ def area
57
+ area = include_rect.area
58
+ return area if exclude_rect.nil?
59
+ area - exclude_rect.area
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,54 @@
1
+ module WatirVisualDiff
2
+ module Modes # :nodoc:
3
+ require 'watir_visual_diff/modes/base'
4
+
5
+ # Compare pixels using Delta E distance.
6
+ class Delta < Base
7
+ attr_reader :tolerance
8
+
9
+ def initialize(options)
10
+ @tolerance = options.delete(:tolerance) || 0.01
11
+ @delta_score = 0.0
12
+ super(options)
13
+ end
14
+
15
+ private
16
+
17
+ def pixels_equal?(a, b)
18
+ a == b
19
+ end
20
+
21
+ def update_result(a, b, x, y)
22
+ d = euclid(a, b) / (MAX * Math.sqrt(3))
23
+ return if d <= tolerance
24
+ @result.diff << [a, b, x, y, d]
25
+ @delta_score += d
26
+ super
27
+ end
28
+
29
+ def background(bg)
30
+ Image.new(bg.width, bg.height, WHITE).with_alpha(0)
31
+ end
32
+
33
+ def euclid(a, b)
34
+ Math.sqrt(
35
+ (r(a) - r(b))**2 +
36
+ (g(a) - g(b))**2 +
37
+ (b(a) - b(b))**2
38
+ )
39
+ end
40
+
41
+ def create_diff_image(bg, diff_image)
42
+ bg.to_grayscale.compose!(diff_image, 0, 0)
43
+ end
44
+
45
+ def pixels_diff(d, *_args, x, y, a)
46
+ d[x, y] = rgba(MAX, 0, 0, (a * MAX).round)
47
+ end
48
+
49
+ def score
50
+ @delta_score / area
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ module WatirVisualDiff
2
+ module Modes # :nodoc:
3
+ require 'watir_visual_diff/modes/base'
4
+
5
+ # Compare pixels by alpha and brightness.
6
+ #
7
+ # Options:
8
+ # - tolerance - defines the maximum allowed difference for alpha/brightness
9
+ # (default value is 16)
10
+ class Grayscale < Base
11
+ DEFAULT_TOLERANCE = 16
12
+
13
+ attr_reader :tolerance
14
+
15
+ def initialize(options)
16
+ @tolerance = options.delete(:tolerance) || DEFAULT_TOLERANCE
17
+ super(options)
18
+ end
19
+
20
+ def pixels_equal?(a, b)
21
+ alpha = color_similar?(a(a), a(b))
22
+ brightness = color_similar?(brightness(a), brightness(b))
23
+ brightness && alpha
24
+ end
25
+
26
+ def update_result(a, b, x, y)
27
+ super
28
+ @result.diff << [a, b, x, y]
29
+ end
30
+
31
+ def background(bg)
32
+ bg.to_grayscale
33
+ end
34
+
35
+ def pixels_diff(d, _a, _b, x, y)
36
+ d[x, y] = rgb(255, 0, 0)
37
+ end
38
+
39
+ def create_diff_image(_bg, diff_image)
40
+ diff_image
41
+ end
42
+
43
+ def color_similar?(a, b)
44
+ d = (a - b).abs
45
+ d <= tolerance
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ module WatirVisualDiff
2
+ module Modes # :nodoc:
3
+ require 'watir_visual_diff/modes/base'
4
+
5
+ # Compare pixels by values.
6
+ # Resulting image contains per-channel differences.
7
+ class RGB < Base
8
+ def pixels_equal?(a, b)
9
+ a == b
10
+ end
11
+
12
+ def update_result(a, b, x, y)
13
+ super
14
+ @result.diff << [a, b, x, y]
15
+ end
16
+
17
+ def background(bg)
18
+ Image.new(bg.width, bg.height, BLACK)
19
+ end
20
+
21
+ def create_diff_image(_bg, diff_image)
22
+ diff_image
23
+ end
24
+
25
+ def pixels_diff(d, a, b, x, y)
26
+ d[x, y] = rgb(
27
+ (r(a) - r(b)).abs,
28
+ (g(a) - g(b)).abs,
29
+ (b(a) - b(b)).abs
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module WatirVisualDiff
2
+ module Modes # :nodoc:
3
+ require 'watir_visual_diff/modes/rgb'
4
+ require 'watir_visual_diff/modes/grayscale'
5
+ require 'watir_visual_diff/modes/delta'
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ module WatirVisualDiff
2
+ class Rectangle
3
+ attr_accessor :left, :top, :right, :bot
4
+
5
+ def initialize(l, t, r, b)
6
+ @left = l
7
+ @top = t
8
+ @right = r
9
+ @bot = b
10
+ end
11
+
12
+ def area
13
+ (right - left + 1) * (bot - top + 1)
14
+ end
15
+
16
+ def contains?(rect)
17
+ (left <= rect.left) &&
18
+ (right >= rect.right) &&
19
+ (top <= rect.top) &&
20
+ (bot >= rect.bot)
21
+ end
22
+
23
+ def bounds
24
+ [left, top, right, bot]
25
+ end
26
+
27
+ def contains_point?(x, y)
28
+ x.between?(left, right) && y.between?(top, bot)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ module WatirVisualDiff
2
+ # Object containing comparison score and diff image
3
+ class Result
4
+ attr_accessor :score, :image
5
+ attr_reader :diff, :mode, :threshold
6
+
7
+ def initialize(mode, threshold)
8
+ @score = 0.0
9
+ @diff = []
10
+ @threshold = threshold
11
+ @mode = mode
12
+ end
13
+
14
+ def difference_image
15
+ @diff_image ||= mode.diff(image, diff)
16
+ end
17
+
18
+ # Returns true iff score less or equals to threshold
19
+ def match?
20
+ score <= threshold
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module WatirVisualDiff # :nodoc:
2
+ VERSION = "0.0.1".freeze
3
+ end
@@ -0,0 +1,14 @@
1
+ require "watir_visual_diff/version"
2
+
3
+ # Compare PNG images using different algorithms
4
+ module WatirVisualDiff
5
+ class SizesMismatchError < StandardError
6
+ end
7
+
8
+ require 'watir_visual_diff/matcher'
9
+ require 'watir_visual_diff/color_methods'
10
+
11
+ def self.compare(path_a, path_b, options = {})
12
+ Matcher.new(options).compare(path_a, path_b)
13
+ end
14
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe WatirVisualDiff::Image do
4
+ describe "highlight_rectangle" do
5
+ let(:image) { described_class.new(10, 10, described_class::BLACK) }
6
+ let(:rect) { WatirVisualDiff::Rectangle.new(0, 0, 1, 1) }
7
+ subject { image.highlight_rectangle(rect, :deep_purple) }
8
+
9
+ it { expect { subject }.to raise_error(ArgumentError) }
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe WatirVisualDiff do
4
+ it 'has a version number' do
5
+ expect(WatirVisualDiff::VERSION).not_to be nil
6
+ end
7
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe WatirVisualDiff::Modes::Delta do
4
+ let(:path_1) { image_path "a" }
5
+ let(:path_2) { image_path "darker" }
6
+ subject { WatirVisualDiff.compare(path_1, path_2, options) }
7
+
8
+ let(:options) { { mode: :delta } }
9
+
10
+ context "with darker image" do
11
+ it "score around 0.075" do
12
+ expect(subject.score).to be_within(0.005).of(0.075)
13
+ end
14
+
15
+ context "with custom threshold" do
16
+ subject { WatirVisualDiff.compare(path_1, path_2, options).match? }
17
+
18
+ context "below score" do
19
+ let(:options) { { mode: :delta, threshold: 0.01 } }
20
+
21
+ it { expect(subject).to be_falsey }
22
+ end
23
+
24
+ context "above score" do
25
+ let(:options) { { mode: :delta, threshold: 0.1 } }
26
+
27
+ it { expect(subject).to be_truthy }
28
+ end
29
+ end
30
+ end
31
+
32
+ context "with different images" do
33
+ let(:path_2) { image_path "b" }
34
+
35
+ it "score around 0.0046" do
36
+ expect(subject.score).to be_within(0.0001).of(0.0046)
37
+ end
38
+
39
+ it "creates correct difference image" do
40
+ expect(subject.difference_image).to eq(WatirVisualDiff::Image.from_file(image_path("delta_diff")))
41
+ end
42
+
43
+ context "with high tolerance" do
44
+ let(:options) { { mode: :delta, tolerance: 0.1 } }
45
+
46
+ it "score around 0.0038" do
47
+ expect(subject.score).to be_within(0.0001).of(0.0038)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ describe WatirVisualDiff::Modes::Grayscale do
4
+ let(:path_1) { image_path "a" }
5
+ let(:path_2) { image_path "darker" }
6
+ subject { WatirVisualDiff.compare(path_1, path_2, options) }
7
+
8
+ let(:options) { { mode: :grayscale } }
9
+
10
+ context "darker image" do
11
+ it "score around 0.95" do
12
+ expect(subject.score).to be_within(0.05).of(0.95)
13
+ end
14
+ end
15
+
16
+ context "different images" do
17
+ let(:path_2) { image_path "b" }
18
+
19
+ it "score around 0.005" do
20
+ expect(subject.score).to be_within(0.001).of(0.005)
21
+ end
22
+
23
+ it "creates correct difference image" do
24
+ expect(subject.difference_image).to eq(WatirVisualDiff::Image.from_file(image_path("grayscale_diff")))
25
+ end
26
+ end
27
+
28
+ context "with zero tolerance" do
29
+ let(:options) { { mode: :grayscale, tolerance: 0 } }
30
+
31
+ context "darker image" do
32
+ it "score equals to 1" do
33
+ expect(subject.score).to eq 1.0
34
+ end
35
+ end
36
+
37
+ context "different image" do
38
+ let(:path_2) { image_path "b" }
39
+
40
+ it "score around 0.016" do
41
+ expect(subject.score).to be_within(0.001).of(0.016)
42
+ end
43
+ end
44
+
45
+ context "equal image" do
46
+ let(:path_2) { image_path "a" }
47
+
48
+ it "score equals to 0" do
49
+ expect(subject.score).to eq 0
50
+ end
51
+ end
52
+ end
53
+
54
+ context "with small tolerance" do
55
+ let(:options) { { mode: :grayscale, tolerance: 8 } }
56
+
57
+ context "darker image" do
58
+ it "score around 0.96" do
59
+ expect(subject.score).to be_within(0.005).of(0.96)
60
+ end
61
+ end
62
+
63
+ context "different image" do
64
+ let(:path_2) { image_path "b" }
65
+
66
+ it "score around 0.006" do
67
+ expect(subject.score).to be_within(0.0005).of(0.006)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe WatirVisualDiff::Modes::RGB do
4
+ let(:path_1) { image_path "a" }
5
+ let(:path_2) { image_path "darker" }
6
+ subject { WatirVisualDiff.compare(path_1, path_2, options) }
7
+ let(:options) { {} }
8
+
9
+ context "with darker" do
10
+ it "score equals to 1" do
11
+ expect(subject.score).to eq 1
12
+ end
13
+ end
14
+
15
+ context "with different images" do
16
+ let(:path_2) { image_path "b" }
17
+
18
+ it "score around 0.016" do
19
+ expect(subject.score).to be_within(0.001).of(0.016)
20
+ end
21
+
22
+ it "creates correct difference image" do
23
+ expect(subject.difference_image).to eq(WatirVisualDiff::Image.from_file(image_path("rgb_diff")))
24
+ end
25
+ end
26
+
27
+ context "exclude rect" do
28
+ let(:options) { { exclude_rect: [200, 150, 275, 200] } }
29
+ let(:path_2) { image_path "a1" }
30
+ it { expect(subject.difference_image).to eq WatirVisualDiff::Image.from_file(image_path("exclude")) }
31
+ it { expect(subject.score).to eq 0 }
32
+
33
+ context "calculates score correctly" do
34
+ let(:path_2) { image_path "darker" }
35
+
36
+ it { expect(subject.score).to eq 1 }
37
+ end
38
+ end
39
+
40
+ context "include rect" do
41
+ let(:options) { { include_rect: [0, 0, 100, 100] } }
42
+ let(:path_2) { image_path "a1" }
43
+ it { expect(subject.difference_image).to eq WatirVisualDiff::Image.from_file(image_path("include")) }
44
+ it { expect(subject.score).to eq 0 }
45
+
46
+ context "calculates score correctly" do
47
+ let(:path_2) { image_path "darker" }
48
+ it { expect(subject.score).to eq 1 }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe WatirVisualDiff::Matcher do
4
+ describe "new" do
5
+ subject { WatirVisualDiff::Matcher.new(options) }
6
+
7
+ context "without options" do
8
+ let(:options) { {} }
9
+
10
+ it { expect(subject.mode.threshold).to eq 0 }
11
+ it { expect(subject.mode).to be_a WatirVisualDiff::Modes::RGB }
12
+ end
13
+
14
+ context "with custom threshold" do
15
+ let(:options) { { threshold: 0.1 } }
16
+
17
+ it { expect(subject.mode.threshold).to eq 0.1 }
18
+ end
19
+
20
+ context "with custom options" do
21
+ let(:options) { { mode: :grayscale, tolerance: 0 } }
22
+
23
+ it { expect(subject.mode.tolerance).to eq 0 }
24
+ end
25
+
26
+ context "with custom mode" do
27
+ let(:options) { { mode: :delta } }
28
+
29
+ it { expect(subject.mode).to be_a WatirVisualDiff::Modes::Delta }
30
+ end
31
+
32
+ context "with undefined mode" do
33
+ let(:options) { { mode: :gamma } }
34
+
35
+ it { expect { subject }.to raise_error(ArgumentError) }
36
+ end
37
+ end
38
+
39
+ describe "compare" do
40
+ let(:path_1) { image_path "very_small" }
41
+ let(:path_2) { image_path "very_small" }
42
+ let(:options) { {} }
43
+ subject { WatirVisualDiff.compare(path_1, path_2, options) }
44
+
45
+ it { expect(subject).to be_a WatirVisualDiff::Result }
46
+
47
+ context "when sizes mismatch" do
48
+ let(:path_2) { image_path "small" }
49
+ it { expect { subject }.to raise_error WatirVisualDiff::SizesMismatchError }
50
+ end
51
+
52
+ context "with negative exclude rect bounds" do
53
+ let(:options) { { exclude_rect: [-1, -1, -1, -1] } }
54
+ it { expect { subject }.to raise_error ArgumentError }
55
+ end
56
+
57
+ context "with big exclude rect bounds" do
58
+ let(:options) { { exclude_rect: [100, 100, 100, 100] } }
59
+ it { expect { subject }.to raise_error ArgumentError }
60
+ end
61
+
62
+ context "with negative include rect bounds" do
63
+ let(:options) { { include_rect: [-1, -1, -1, -1] } }
64
+ it { expect { subject }.to raise_error ArgumentError }
65
+ end
66
+
67
+ context "with big include rect bounds" do
68
+ let(:options) { { include_rect: [100, 100, 100, 100] } }
69
+ it { expect { subject }.to raise_error ArgumentError }
70
+ end
71
+
72
+ context "with wrong include and exclude rects combination" do
73
+ let(:options) { { include_rect: [1, 1, 2, 2], exclude_rect: [0, 0, 1, 1] } }
74
+ it { expect { subject }.to raise_error ArgumentError }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe WatirVisualDiff::Rectangle do
4
+ let(:rect) { described_class.new(0, 0, 9, 9) }
5
+
6
+ describe 'area' do
7
+ subject { rect.area }
8
+
9
+ it { expect(subject).to eq 100 }
10
+ end
11
+
12
+ describe "contains?" do
13
+ let(:rect2) { described_class.new(1, 1, 8, 8) }
14
+ subject { rect.contains?(rect2) }
15
+
16
+ it { expect(subject).to be_truthy }
17
+
18
+ context "when does not contain" do
19
+ let(:rect2) { described_class.new(2, 2, 10, 10) }
20
+
21
+ it { expect(subject).to be_falsey }
22
+ end
23
+ end
24
+
25
+ describe "contains_point?" do
26
+ let(:point) { [5, 5] }
27
+ subject { rect.contains_point?(*point) }
28
+
29
+ it { expect(subject).to be_truthy }
30
+
31
+ context "when does not contain" do
32
+ let(:point) { [10, 10] }
33
+
34
+ it { expect(subject).to be_falsey }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'rspec'
4
+ require 'watir_visual_diff'
5
+
6
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
7
+
8
+ RSpec.configure do |config|
9
+ config.mock_with :rspec
10
+ end
11
+
12
+ def image_path(name)
13
+ "#{File.dirname(__FILE__)}/fixtures/#{name}.png"
14
+ end
@@ -0,0 +1,21 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require 'watir_visual_diff/version'
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "watir_visual_diff"
6
+ spec.version = WatirVisualDiff::VERSION
7
+ spec.authors = ["Justin Commu"]
8
+ spec.email = ["jcommu@gmail.com"]
9
+ spec.summary = "Image comparison library for Watir"
10
+ spec.description = "Image comparison libray for Watir built on top of OilyPNG"
11
+ spec.homepage = "https://github.com/automation-wizards/watir_visual_diff"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.require_paths = ["lib"]
16
+
17
+ spec.add_dependency "oily_png", "~> 1.2.1"
18
+
19
+ spec.add_development_dependency "rake", "~> 10.0"
20
+ spec.add_development_dependency "rspec", "~> 3.0"
21
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: watir_visual_diff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Justin Commu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oily_png
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Image comparison libray for Watir built on top of OilyPNG
56
+ email:
57
+ - jcommu@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".rubocop.yml"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - examples/performance.rb
73
+ - lib/watir_visual_diff.rb
74
+ - lib/watir_visual_diff/color_methods.rb
75
+ - lib/watir_visual_diff/image.rb
76
+ - lib/watir_visual_diff/matcher.rb
77
+ - lib/watir_visual_diff/modes.rb
78
+ - lib/watir_visual_diff/modes/base.rb
79
+ - lib/watir_visual_diff/modes/delta.rb
80
+ - lib/watir_visual_diff/modes/grayscale.rb
81
+ - lib/watir_visual_diff/modes/rgb.rb
82
+ - lib/watir_visual_diff/rectangle.rb
83
+ - lib/watir_visual_diff/result.rb
84
+ - lib/watir_visual_diff/version.rb
85
+ - spec/fixtures/a.png
86
+ - spec/fixtures/a1.png
87
+ - spec/fixtures/b.png
88
+ - spec/fixtures/darker.png
89
+ - spec/fixtures/delta_diff.png
90
+ - spec/fixtures/exclude.png
91
+ - spec/fixtures/grayscale_diff.png
92
+ - spec/fixtures/include.png
93
+ - spec/fixtures/rgb_diff.png
94
+ - spec/fixtures/small.png
95
+ - spec/fixtures/very_small.png
96
+ - spec/image_spec.rb
97
+ - spec/imatcher_spec.rb
98
+ - spec/integrations/delta_spec.rb
99
+ - spec/integrations/grayscale_spec.rb
100
+ - spec/integrations/rgb_spec.rb
101
+ - spec/matcher_spec.rb
102
+ - spec/rectangle_spec.rb
103
+ - spec/spec_helper.rb
104
+ - watir_visual_diff.gemspec
105
+ homepage: https://github.com/automation-wizards/watir_visual_diff
106
+ licenses:
107
+ - MIT
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.6.7
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Image comparison library for Watir
129
+ test_files: []