diffux-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 80f32154fc059c048c223e8e2ca3646a802a8859
4
+ data.tar.gz: 7452b31f9989d95cbc3799d5bcacbe1e4dcd99b7
5
+ SHA512:
6
+ metadata.gz: 240ad46905dfd690d5c3fcafc82eff833ba0c0128a3e786c08089dc40cf33b720efda4f9d971e70f2767eb975829d7a5971981983c5b5be2acaf352ae923a383
7
+ data.tar.gz: 60e6f38b354ad99791fad9c333ed52e9e038f7eee7ac823c3eebcd92ee84294dfae794436f64e0717babf168e896380dfd08ee66b92564ce23957fef2fd42a3c
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'diffux_core'
4
+ require 'oily_png'
5
+ require 'optparse'
6
+
7
+ options = {
8
+ outfile: 'diff.png'
9
+ }
10
+ OptionParser.new do |opts|
11
+ opts.banner = 'Usage: diffux-compare [options]'
12
+ opts.separator ''
13
+ opts.separator 'Options:'
14
+
15
+ opts.on('-b', '--before-image BEFORE_IMAGE',
16
+ 'Specify a path to the before-image') do |img|
17
+ options[:before_image] = img
18
+ end
19
+ opts.on('-a', '--after-image AFTER_IMAGE',
20
+ 'Specify a path to the after-image') do |img|
21
+ options[:after_image] = img
22
+ end
23
+
24
+ opts.on('-o', '--outfile OUTFILE',
25
+ 'Specify where the diff image will be saved (if needed)') do |f|
26
+ options[:outfile] = f
27
+ end
28
+
29
+ opts.on_tail('-h', '--help', 'Show this message') do
30
+ puts opts
31
+ exit
32
+ end
33
+ end.parse!(ARGV)
34
+
35
+ help = '(use `diffux-compare -h` to see options)'
36
+ unless options[:before_image]
37
+ puts "Missing BEFORE_IMAGE #{help}"
38
+ exit 1
39
+ end
40
+ unless options[:after_image]
41
+ puts "Missing AFTER_IMAGE #{help}"
42
+ exit 1
43
+ end
44
+
45
+ comparison = Diffux::SnapshotComparer.new(
46
+ ChunkyPNG::Image.from_file(options[:before_image]),
47
+ ChunkyPNG::Image.from_file(options[:after_image]),
48
+ ).compare!
49
+
50
+ if img = comparison[:diff_image]
51
+ img.save(options[:outfile])
52
+ puts "DIFF: #{comparison[:diff_in_percent]}%"
53
+ exit 10
54
+ end
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'diffux_core'
4
+ require 'optparse'
5
+
6
+ options = {
7
+ width: 320
8
+ }
9
+ OptionParser.new do |opts|
10
+ opts.banner = 'Usage: diffux-snapshot [options]'
11
+ opts.separator ''
12
+ opts.separator 'Options:'
13
+
14
+ opts.on('-u', '--url URL',
15
+ 'Set a URL to snapshot') do |url|
16
+ options[:url] = url
17
+ end
18
+
19
+ opts.on('-o', '--outfile OUTFILE',
20
+ 'Specify where the snapshot will be saved') do |f|
21
+ options[:outfile] = f
22
+ end
23
+
24
+ opts.on('-w', '--width WIDTH', Integer,
25
+ 'Set the width of the screen (defaults to 320)') do |w|
26
+ options[:width] = w
27
+ end
28
+
29
+ opts.on('-a', '--useragent USERAGENT',
30
+ 'Set a useragent header when loading the url') do |ua|
31
+ options[:useragent] = ua
32
+ end
33
+
34
+ opts.on_tail('-h', '--help', 'Show this message') do
35
+ puts opts
36
+ exit
37
+ end
38
+ end.parse!(ARGV)
39
+
40
+ help = '(use `diffux-snapshot -h` to see options)'
41
+ unless options[:url]
42
+ puts "Missing url #{help}"
43
+ exit 1
44
+ end
45
+ unless options[:outfile]
46
+ puts "Missing outfile #{help}"
47
+ exit 1
48
+ end
49
+
50
+ snapshot = Diffux::Snapshotter.new(
51
+ viewport_width: options[:width],
52
+ user_agent: options[:useragent],
53
+ outfile: options[:outfile],
54
+ url: options[:url],
55
+ ).take_snapshot!
56
+
@@ -0,0 +1,3 @@
1
+ require 'diffux_core/version'
2
+ require 'diffux_core/snapshotter'
3
+ require 'diffux_core/snapshot_comparer'
@@ -0,0 +1,47 @@
1
+ require 'set'
2
+ module Diffux
3
+ # This class finds clusters in a diff. A cluster is defined as rows that are
4
+ # different, and are closer than DIFF_ROW_THRESHOLD pixels to its neighboring
5
+ # diff row.
6
+ class DiffClusterFinder
7
+ MAXIMUM_ADJACENCY_GAP = 5
8
+
9
+ # @param number_of_rows [Numeric]
10
+ def initialize(number_of_rows)
11
+ @number_of_rows = number_of_rows
12
+ @rows_with_diff = SortedSet.new
13
+ end
14
+
15
+ # Tell the DiffClusterFinder about a row that is different.
16
+ #
17
+ # @param row [Numeric]
18
+ def row_is_different(row)
19
+ @rows_with_diff.add row
20
+ end
21
+
22
+ # @return [Float] the percent of rows that are different
23
+ def percent_of_rows_different
24
+ @rows_with_diff.length.to_f / @number_of_rows * 100
25
+ end
26
+
27
+ # Calculate clusters from diff-rows that are close to each other.
28
+ #
29
+ # @return [Array<Hash>] a list of clusters modeled as hashes:
30
+ # `{ start: x, finish: y }`
31
+ def clusters
32
+ results = []
33
+ @rows_with_diff.each do |row|
34
+ current = results.last
35
+ if !current || current[:finish] + MAXIMUM_ADJACENCY_GAP < row
36
+ results << {
37
+ start: row,
38
+ finish: row,
39
+ }
40
+ else
41
+ current[:finish] = row
42
+ end
43
+ end
44
+ results
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,72 @@
1
+ require 'oily_png'
2
+ require 'diff-lcs'
3
+ require_relative 'diff_cluster_finder'
4
+
5
+ module Diffux
6
+ # This class is responsible for comparing two Snapshots and generating a diff.
7
+ class SnapshotComparer
8
+ # @param png_before [ChunkyPNG::Image]
9
+ # @param png_after [ChunkyPNG::Image]
10
+ def initialize(png_before, png_after)
11
+ @png_after = png_after
12
+ @png_before = png_before
13
+ end
14
+
15
+ # @return [Hash]
16
+ def compare!
17
+ sdiff = Diff::LCS.sdiff(to_array_of_arrays(@png_before),
18
+ to_array_of_arrays(@png_after))
19
+ cluster_finder = DiffClusterFinder.new(sdiff.size)
20
+ sprite, all_comparisons = initialize_comparison_images(
21
+ [@png_after.width, @png_before.width].max, sdiff.size)
22
+
23
+ sdiff.each_with_index do |row, y|
24
+ # each row is a Diff::LCS::ContextChange instance
25
+ all_comparisons.each { |image| image.render_row(y, row) }
26
+ cluster_finder.row_is_different(y) unless row.unchanged?
27
+ end
28
+
29
+ percent_changed = cluster_finder.percent_of_rows_different
30
+ {
31
+ diff_in_percent: percent_changed,
32
+ diff_image: (sprite if percent_changed > 0),
33
+ diff_clusters: cluster_finder.clusters,
34
+ }
35
+ end
36
+
37
+ private
38
+
39
+ # @param [ChunkyPNG::Image]
40
+ # @return [Array<Array<Integer>>]
41
+ def to_array_of_arrays(chunky_png)
42
+ array_of_arrays = []
43
+ chunky_png.height.times do |y|
44
+ array_of_arrays << chunky_png.row(y)
45
+ end
46
+ array_of_arrays
47
+ end
48
+
49
+ # @param canvas [ChunkyPNG::Image] The output image to draw pixels on
50
+ # @return [Array<SnapshotComparisonImage>]
51
+ def initialize_comparison_images(width, height)
52
+ gutter_width = SnapshotComparisonImage::Gutter::WIDTH
53
+ total_width = (width * 3) + (gutter_width * 3)
54
+
55
+ sprite = ChunkyPNG::Image.new(total_width, height)
56
+ offset, comparison_images = 0, []
57
+ comparison_images << SnapshotComparisonImage::Gutter.new(offset, sprite)
58
+ offset += gutter_width
59
+ comparison_images << SnapshotComparisonImage::Before.new(offset, sprite)
60
+ offset += width
61
+ comparison_images << SnapshotComparisonImage::Gutter.new(offset, sprite)
62
+ offset += gutter_width
63
+ comparison_images << SnapshotComparisonImage::Overlayed.new(offset, sprite)
64
+ offset += width
65
+ comparison_images << SnapshotComparisonImage::Gutter.new(offset, sprite)
66
+ offset += gutter_width
67
+ comparison_images << SnapshotComparisonImage::After.new(offset, sprite)
68
+
69
+ [sprite, comparison_images]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,21 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This subclass of `SnapshotComparisonImage` knows how to draw the
4
+ # representation of the "after" image.
5
+ class After < SnapshotComparisonImage::Base
6
+ # @param y [Integer]
7
+ # @param row [Diff::LCS:ContextChange]
8
+ def render_changed_row(y, row)
9
+ render_added_row(y, row)
10
+ end
11
+
12
+ # @param y [Integer]
13
+ # @param row [Diff::LCS:ContextChange]
14
+ def render_added_row(y, row)
15
+ row.new_element.each_with_index do |pixel_after, x|
16
+ render_pixel(x, y, pixel_after)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,115 @@
1
+ require 'chunky_png'
2
+ module Diffux
3
+ module SnapshotComparisonImage
4
+ # This model represents a "comparison image". Basically it's just a wrapper
5
+ # around a ChunkyPNG image with some nice methods to make life easier in the
6
+ # world of diffs.
7
+ #
8
+ # This model is never persisted.
9
+ class Base
10
+ include ChunkyPNG::Color
11
+
12
+ BASE_OPACITY = 0.1
13
+ BASE_ALPHA = (255 * BASE_OPACITY).round
14
+ BASE_DIFF_ALPHA = BASE_ALPHA * 2
15
+
16
+ MAGENTA = ChunkyPNG::Color.from_hex '#b33682'
17
+ RED = ChunkyPNG::Color.from_hex '#dc322f'
18
+ GREEN = ChunkyPNG::Color.from_hex '#859900'
19
+
20
+ # @param offset [Integer] the x-offset that this comparison image should
21
+ # use when rendering on the canvas image.
22
+ # @param canvas [ChunkyPNG::Image] The canvas image to render pixels on.
23
+ def initialize(offset, canvas)
24
+ @offset = offset
25
+ @canvas = canvas
26
+ end
27
+
28
+ # @param y [Integer]
29
+ # @param row [Diff::LCS:ContextChange]
30
+ def render_row(y, row)
31
+ if row.unchanged?
32
+ render_unchanged_row(y, row)
33
+ elsif row.deleting?
34
+ render_deleted_row(y, row)
35
+ elsif row.adding?
36
+ render_added_row(y, row)
37
+ else # changing?
38
+ render_changed_row(y, row)
39
+ end
40
+ end
41
+
42
+ # @param y [Integer]
43
+ # @param row [Diff::LCS:ContextChange]
44
+ def render_unchanged_row(y, row)
45
+ row.new_element.each_with_index do |pixel, x|
46
+ # Render the unchanged pixel as-is
47
+ render_pixel(x, y, pixel)
48
+ end
49
+ end
50
+
51
+ # @param y [Integer]
52
+ # @param row [Diff::LCS:ContextChange]
53
+ def render_changed_row(y, row)
54
+ # no default implementation
55
+ end
56
+
57
+ # @param y [Integer]
58
+ # @param row [Diff::LCS:ContextChange]
59
+ def render_added_row(y, row)
60
+ # no default implementation
61
+ end
62
+
63
+ # @param y [Integer]
64
+ # @param row [Diff::LCS:ContextChange]
65
+ def render_deleted_row(y, row)
66
+ # no default implementation
67
+ end
68
+
69
+ # Could be simplified as ChunkyPNG::Color::MAX * 2, but this format mirrors
70
+ # the math in #pixel_diff_score
71
+ MAX_EUCLIDEAN_DISTANCE = Math.sqrt(ChunkyPNG::Color::MAX**2 * 4)
72
+
73
+ # Compute a score that represents the difference between 2 pixels
74
+ #
75
+ # This method simply takes the Euclidean distance between the RGBA channels
76
+ # of 2 colors over the maximum possible Euclidean distance. This gives us a
77
+ # percentage of how different the two colors are.
78
+ #
79
+ # Although it would be more perceptually accurate to calculate a proper
80
+ # Delta E in Lab colorspace, we probably don't need perceptual accuracy for
81
+ # this application, and it is nice to avoid the overhead of converting RGBA
82
+ # to Lab.
83
+ #
84
+ # @param pixel_after [Integer]
85
+ # @param pixel_before [Integer]
86
+ # @return [Float] number between 0 and 1 where 1 is completely different
87
+ # and 0 is no difference
88
+ def pixel_diff_score(pixel_after, pixel_before)
89
+ Math.sqrt(
90
+ (r(pixel_after) - r(pixel_before))**2 +
91
+ (g(pixel_after) - g(pixel_before))**2 +
92
+ (b(pixel_after) - b(pixel_before))**2 +
93
+ (a(pixel_after) - a(pixel_before))**2
94
+ ) / MAX_EUCLIDEAN_DISTANCE
95
+ end
96
+
97
+ # @param diff_score [Float]
98
+ # @return [Integer] a number between 0 and 255 that represents the alpha
99
+ # channel of of the difference
100
+ def diff_alpha(diff_score)
101
+ (BASE_DIFF_ALPHA + ((255 - BASE_DIFF_ALPHA) * diff_score)).round
102
+ end
103
+
104
+ # Renders a pixel on the specified x and y position. Uses the offset that
105
+ # the comparison image has been configured with.
106
+ #
107
+ # @param x [Integer]
108
+ # @param y [Integer]
109
+ # @param pixel [Integer]
110
+ def render_pixel(x, y, pixel)
111
+ @canvas.set_pixel(x + @offset, y, pixel)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,21 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This subclass of `SnapshotComparisonImage` knows how to draw the
4
+ # representation of the "before" image.
5
+ class Before < SnapshotComparisonImage::Base
6
+ # @param y [Integer]
7
+ # @param row [Diff::LCS:ContextChange]
8
+ def render_changed_row(y, row)
9
+ render_deleted_row(y, row)
10
+ end
11
+
12
+ # @param y [Integer]
13
+ # @param row [Diff::LCS:ContextChange]
14
+ def render_deleted_row(y, row)
15
+ row.old_element.each_with_index do |pixel_before, x|
16
+ render_pixel(x, y, pixel_before)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This class renders a gutter-column with a color representing the type of
4
+ # change that has happened.
5
+ class Gutter < SnapshotComparisonImage::Base
6
+ WIDTH = 10
7
+ GRAY = ChunkyPNG::Color.from_hex '#cccccc'
8
+
9
+ def render_row(y, row)
10
+ WIDTH.times do |x|
11
+ render_pixel(x, y, gutter_color(row))
12
+ end
13
+ # render a two-pixel empty column
14
+ 2.times do |x|
15
+ render_pixel(WIDTH - 1 - x, y, WHITE)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def gutter_color(row)
22
+ if row.unchanged?
23
+ WHITE
24
+ elsif row.deleting?
25
+ RED
26
+ elsif row.adding?
27
+ GREEN
28
+ else # changed?
29
+ GRAY
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,88 @@
1
+ module Diffux
2
+ module SnapshotComparisonImage
3
+ # This subclass of `SnapshotComparisonImage` knows how to overlay the
4
+ # after-image on top of the before-image, and render the difference in a
5
+ # scaled magenta color.
6
+ class Overlayed < SnapshotComparisonImage::Base
7
+ WHITE_OVERLAY = ChunkyPNG::Color.fade(WHITE, 1 - BASE_ALPHA)
8
+
9
+ # @param offset [Integer]
10
+ # @param canvas [ChunkyPNG::Image]
11
+ # @see SnapshotComparisonImage::Base
12
+ def initialize(offset, canvas)
13
+ @diff_pixels = {}
14
+ @faded_pixels = {}
15
+ super
16
+ end
17
+
18
+ # @param y [Integer]
19
+ # @param row [Diff::LCS:ContextChange]
20
+ def render_unchanged_row(y, row)
21
+ # Render translucent original pixels
22
+ row.new_element.each_with_index do |pixel, x|
23
+ render_faded_pixel(x, y, pixel)
24
+ end
25
+ end
26
+
27
+ # @param y [Integer]
28
+ # @param row [Diff::LCS:ContextChange]
29
+ def render_deleted_row(y, row)
30
+ row.old_element.each_with_index do |pixel_before, x|
31
+ render_faded_magenta_pixel(TRANSPARENT, pixel_before, x, y)
32
+ end
33
+ end
34
+
35
+ # @param y [Integer]
36
+ # @param row [Diff::LCS:ContextChange]
37
+ def render_added_row(y, row)
38
+ row.new_element.each_with_index do |pixel_after, x|
39
+ render_faded_magenta_pixel(pixel_after, TRANSPARENT, x, y)
40
+ end
41
+ end
42
+
43
+ # @param y [Integer]
44
+ # @param row [Diff::LCS:ContextChange]
45
+ def render_changed_row(y, row)
46
+ row.old_element.zip(row.new_element).each_with_index do |pixels, x|
47
+ pixel_before, pixel_after = pixels
48
+ render_faded_magenta_pixel(
49
+ pixel_after || TRANSPARENT,
50
+ pixel_before || TRANSPARENT,
51
+ x, y)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # @param pixel_after [Integer]
58
+ # @param pixel_before [Integer]
59
+ # @param x [Integer]
60
+ # @param y [Integer]
61
+ def render_faded_magenta_pixel(pixel_after, pixel_before, x, y)
62
+ score = pixel_diff_score(pixel_after, pixel_before)
63
+ if score > 0
64
+ render_diff_pixel(x, y, score)
65
+ else
66
+ render_faded_pixel(x, y, pixel_after)
67
+ end
68
+ end
69
+
70
+ # @param x [Integer]
71
+ # @param y [Integer]
72
+ # @param score [Float]
73
+ def render_diff_pixel(x, y, score)
74
+ @diff_pixels[score] ||= compose_quick(fade(MAGENTA, diff_alpha(score)),
75
+ WHITE)
76
+ render_pixel(x, y, @diff_pixels[score])
77
+ end
78
+
79
+ # @param x [Integer]
80
+ # @param y [Integer]
81
+ # @param pixel [Integer]
82
+ def render_faded_pixel(x, y, pixel)
83
+ @faded_pixels[pixel] ||= compose_quick(WHITE_OVERLAY, pixel)
84
+ render_pixel(x, y, @faded_pixels[pixel])
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,66 @@
1
+ require 'phantomjs'
2
+ require 'json'
3
+ %w(base gutter before after overlayed).each do |type|
4
+ require_relative "snapshot_comparison_image/#{type}"
5
+ end
6
+ module Diffux
7
+ # Snapshotter is responsible for delegating to PhantomJS to take the snapshot
8
+ # for a given URL and viewoprt, and then saving that snapshot to a file and
9
+ # storing any metadata on the Snapshot object.
10
+ class Snapshotter
11
+ SCRIPT_PATH = File.join(File.dirname(__FILE__), 'script/take-snapshot.js').to_s
12
+
13
+ # @param url [String} the URL to snapshot
14
+ # @param viewport_width [Integer] the width of the screen used when
15
+ # snapshotting
16
+ # @param outfile [File] where to store the snapshot PNG.
17
+ # @param user_agent [String] an optional useragent string to used when
18
+ # requesting the page.
19
+ def initialize(url: raise, viewport_width: raise,
20
+ outfile: raise, user_agent: nil)
21
+ @viewport_width = viewport_width
22
+ @user_agent = user_agent
23
+ @outfile = outfile
24
+ @url = url
25
+ end
26
+
27
+ # Takes a snapshot of the URL and saves it in the out_file as a PNG image.
28
+ #
29
+ # @return [Hash] a hash containing the following keys:
30
+ # title [String] the <title> of the page being snapshotted
31
+ # log [String] a log of events happened during the snapshotting process
32
+ def take_snapshot!
33
+ result = {}
34
+ opts = {
35
+ address: @url,
36
+ outfile: @outfile,
37
+ viewportSize: {
38
+ width: @viewport_width,
39
+ height: @viewport_width,
40
+ },
41
+ }
42
+ opts[:userAgent] = @user_agent if @user_agent
43
+
44
+ run_phantomjs(opts) do |line|
45
+ begin
46
+ result = JSON.parse line, symbolize_names: true
47
+ rescue JSON::ParserError
48
+ # We only expect a single line of JSON to be output by our snapshot
49
+ # script. If something else is happening, it is likely a JavaScript
50
+ # error on the page and we should just forget about it and move on
51
+ # with our lives.
52
+ end
53
+ end
54
+ result
55
+ end
56
+
57
+ private
58
+
59
+ def run_phantomjs(options)
60
+ Phantomjs.run('--ignore-ssl-errors=true',
61
+ SCRIPT_PATH, options.to_json) do |line|
62
+ yield line
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,4 @@
1
+ # Defines the gem version.
2
+ module DiffuxCore
3
+ VERSION = '0.0.1'
4
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: diffux-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Lencioni
8
+ - Henric Trotzig
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: oily_png
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 1.1.1
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 1.1.1
28
+ - !ruby/object:Gem::Dependency
29
+ name: phantomjs
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '='
33
+ - !ruby/object:Gem::Version
34
+ version: 1.9.2.1
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '='
40
+ - !ruby/object:Gem::Version
41
+ version: 1.9.2.1
42
+ - !ruby/object:Gem::Dependency
43
+ name: diff-lcs
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 1.2.5
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 1.2.5
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.14.1
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 2.14.1
70
+ - !ruby/object:Gem::Dependency
71
+ name: mocha
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 1.0.0
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 1.0.0
84
+ description: Tools for taking and comparing responsive website snapshots
85
+ email:
86
+ - joe.lencioni@causes.com
87
+ - henric.trotzig@causes.com
88
+ executables:
89
+ - diffux-snapshot
90
+ - diffux-compare
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - bin/diffux-compare
95
+ - bin/diffux-snapshot
96
+ - lib/diffux_core.rb
97
+ - lib/diffux_core/diff_cluster_finder.rb
98
+ - lib/diffux_core/snapshot_comparer.rb
99
+ - lib/diffux_core/snapshot_comparison_image/after.rb
100
+ - lib/diffux_core/snapshot_comparison_image/base.rb
101
+ - lib/diffux_core/snapshot_comparison_image/before.rb
102
+ - lib/diffux_core/snapshot_comparison_image/gutter.rb
103
+ - lib/diffux_core/snapshot_comparison_image/overlayed.rb
104
+ - lib/diffux_core/snapshotter.rb
105
+ - lib/diffux_core/version.rb
106
+ homepage:
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 2.0.0
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.2.0
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Diffux Core
130
+ test_files: []