diffux-core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/diffux-compare +54 -0
- data/bin/diffux-snapshot +56 -0
- data/lib/diffux_core.rb +3 -0
- data/lib/diffux_core/diff_cluster_finder.rb +47 -0
- data/lib/diffux_core/snapshot_comparer.rb +72 -0
- data/lib/diffux_core/snapshot_comparison_image/after.rb +21 -0
- data/lib/diffux_core/snapshot_comparison_image/base.rb +115 -0
- data/lib/diffux_core/snapshot_comparison_image/before.rb +21 -0
- data/lib/diffux_core/snapshot_comparison_image/gutter.rb +34 -0
- data/lib/diffux_core/snapshot_comparison_image/overlayed.rb +88 -0
- data/lib/diffux_core/snapshotter.rb +66 -0
- data/lib/diffux_core/version.rb +4 -0
- metadata +130 -0
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
|
data/bin/diffux-compare
ADDED
@@ -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
|
data/bin/diffux-snapshot
ADDED
@@ -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
|
+
|
data/lib/diffux_core.rb
ADDED
@@ -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
|
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: []
|