watir_visual_diff 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []