diffux-core 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.
- 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: []
|