Class: SnapDiff::Drivers::VipsDriver

Inherits:
Object
  • Object
show all
Includes:
SnapDiff::Driver
Defined in:
lib/snap_diff/drivers/vips_driver.rb

Constant Summary collapse

REVALIDATE =

libvips caches loader operations keyed on filename + mtime, and mtime has ONE-SECOND resolution -- so overwriting a path and re-reading it within the same second hands back the PREVIOUS image. This gem does exactly that: the screenshoter writes <name>.png, checkout_base_screenshot writes <name>.base.png from VCS, and the comparison then reads both.

revalidate: true tells the loader to skip the cached result (libvips 8.15+).

Vips.at_least_libvips?(8, 15) ? {revalidate: true}.freeze : {}.freeze
MAX_FILENAME_LENGTH =
200

Constants included from SnapDiff::Driver

SnapDiff::Driver::PNG_EXTENSION

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SnapDiff::Driver

#dimension, #height_for, #image_area_size, included, #same_dimension?, #supports?, #width_for

Class Method Details

.alpha_channel_diff(base_image, new_image) ⇒ Object



155
156
157
158
159
# File 'lib/snap_diff/drivers/vips_driver.rb', line 155

def alpha_channel_diff(base_image, new_image)
  return unless base_image.bands > 3 && new_image.bands > 3

  (base_image.extract_band(3) - new_image.extract_band(3)).abs > 0
end

.difference_area_size_by(difference_mask) ⇒ Object



129
130
131
132
# File 'lib/snap_diff/drivers/vips_driver.rb', line 129

def difference_area_size_by(difference_mask)
  diff_mask = difference_mask == 0
  diff_mask.hist_find.to_a[0][0].max
end

.difference_mask(base_image, new_image, color_distance = nil) ⇒ Object



134
135
136
137
# File 'lib/snap_diff/drivers/vips_driver.rb', line 134

def difference_mask(base_image, new_image, color_distance = nil)
  result = (new_image - base_image).abs
  color_distance ? result > color_distance : result
end

.difference_region_by(diff_mask) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/snap_diff/drivers/vips_driver.rb', line 161

def difference_region_by(diff_mask)
  columns, rows = diff_mask.bandor.project

  left = columns.profile[1].min
  right = columns.width - columns.flip(:horizontal).profile[1].min

  top = rows.profile[0].min
  bottom = rows.height - rows.flip(:vertical).profile[0].min

  return nil if right < left || bottom < top

  Region.from_edge_coordinates(left, top, right, bottom)
end

.perceptual_color_diff(base_image, new_image) ⇒ Object



145
146
147
148
149
150
151
152
153
# File 'lib/snap_diff/drivers/vips_driver.rb', line 145

def perceptual_color_diff(base_image, new_image)
  base_rgb = (base_image.bands > 3) ? base_image.extract_band(0, n: 3) : base_image
  new_rgb = (new_image.bands > 3) ? new_image.extract_band(0, n: 3) : new_image
  base_lab = base_rgb.colourspace(:lab)
  new_lab = new_rgb.colourspace(:lab)
  base_lab.dE00(new_lab)
rescue Vips::Error
  base_lab.dE76(new_lab)
end

.perceptual_difference_mask(base_image, new_image, threshold = 2.0) ⇒ Object



139
140
141
142
143
# File 'lib/snap_diff/drivers/vips_driver.rb', line 139

def perceptual_difference_mask(base_image, new_image, threshold = 2.0)
  color_diff = perceptual_color_diff(base_image, new_image) > threshold
  alpha_diff = alpha_channel_diff(base_image, new_image)
  alpha_diff ? (color_diff | alpha_diff) : color_diff
end

Instance Method Details

#add_black_box(memo, region) ⇒ Object



68
69
70
71
72
# File 'lib/snap_diff/drivers/vips_driver.rb', line 68

def add_black_box(memo, region)
  return memo unless region

  memo.draw_rect([0, 0, 0, 0], *region.to_top_left_corner_coordinates, fill: true)
end

#crop(region, i) ⇒ Object



54
55
56
57
58
59
60
61
62
# File 'lib/snap_diff/drivers/vips_driver.rb', line 54

def crop(region, i)
  i.crop(*region.to_top_left_corner_coordinates)
rescue Vips::Error => e
  warn(
    "[capybara-screenshot-diff] Crop has been failed for " \
      "{ region: #{region.to_top_left_corner_coordinates.inspect}, image: #{dimension(i).join("x")} }"
  )
  raise e
end

#difference_level(diff_mask, old_img, _region = nil) ⇒ Object



74
75
76
# File 'lib/snap_diff/drivers/vips_driver.rb', line 74

def difference_level(diff_mask, old_img, _region = nil)
  self.class.difference_area_size_by(diff_mask).to_f / image_area_size(old_img)
end

#draw_rectangles(images, region, rgba, offset: 0) ⇒ Object



108
109
110
111
112
# File 'lib/snap_diff/drivers/vips_driver.rb', line 108

def draw_rectangles(images, region, rgba, offset: 0)
  images.map do |image|
    image.draw_rect(rgba, region.left - offset, region.top - offset, region.width + (offset * 2), region.height + (offset * 2))
  end
end

#filter_image_with_median(image, median_filter_window_size) ⇒ Object



64
65
66
# File 'lib/snap_diff/drivers/vips_driver.rb', line 64

def filter_image_with_median(image, median_filter_window_size)
  image.median(median_filter_window_size)
end

#find_difference_region(comparison) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/snap_diff/drivers/vips_driver.rb', line 32

def find_difference_region(comparison)
  new_image, base_image, options = comparison.new_image, comparison.base_image, comparison.options

  diff_mask = if options[:perceptual_threshold]
    self.class.perceptual_difference_mask(base_image, new_image, options[:perceptual_threshold])
  else
    self.class.difference_mask(base_image, new_image, options[:color_distance_limit])
  end
  region = self.class.difference_region_by(diff_mask)
  # TODO: schedule research when we got this case for VIPs
  # region = nil if region && region_covers_entire_image?(region, base_image)

  result = ComparisonResult.new(region, {}, comparison)

  unless result.blank?
    result.meta[:difference_level] = difference_level(diff_mask, base_image) if comparison.options[:tolerance]
    result.meta[:diff_mask] = diff_mask
  end

  result
end

#from_file(filename) ⇒ Object



99
100
101
102
103
104
105
106
# File 'lib/snap_diff/drivers/vips_driver.rb', line 99

def from_file(filename)
  result = ::Vips::Image.new_from_file(filename.to_s, **REVALIDATE)

  result = result.colourspace(:srgb) if result.bands < 3
  result = result.bandjoin(255) if result.bands == 3

  result
end

#highlight_mask(diff_mask, merged_image, color: SnapDiff::RED_RGBA) ⇒ Object



122
123
124
# File 'lib/snap_diff/drivers/vips_driver.rb', line 122

def highlight_mask(diff_mask, merged_image, color: SnapDiff::RED_RGBA)
  diff_mask.ifthenelse(color, merged_image * 0.75)
end

#load_images(old_file_name, new_file_name) ⇒ Object



95
96
97
# File 'lib/snap_diff/drivers/vips_driver.rb', line 95

def load_images(old_file_name, new_file_name)
  [from_file(old_file_name), from_file(new_file_name)]
end

#merge(new_image, base_image) ⇒ Object



118
119
120
# File 'lib/snap_diff/drivers/vips_driver.rb', line 118

def merge(new_image, base_image)
  base_image.composite2(new_image, :over)
end

#resize_image_to(image, new_width, new_height) ⇒ Object



91
92
93
# File 'lib/snap_diff/drivers/vips_driver.rb', line 91

def resize_image_to(image, new_width, new_height)
  image.resize(new_width.to_f / image.width, vscale: new_height.to_f / image.height)
end

#same_pixels?(comparison) ⇒ Boolean

Returns:

  • (Boolean)


114
115
116
# File 'lib/snap_diff/drivers/vips_driver.rb', line 114

def same_pixels?(comparison)
  (comparison.new_image == comparison.base_image).min == 255
end

#save_image_to(image, filename) ⇒ Object

Vips could not work with the same file. Per each process we require to create new file



81
82
83
84
85
86
87
88
89
# File 'lib/snap_diff/drivers/vips_driver.rb', line 81

def save_image_to(image, filename)
  # Dir::Tmpname will happily produce tempfile names that are too long for most unix filesystems,
  # which leads to "unix error: File name too long". Apply a limit to avoid this.
  limited_filename = filename.to_s[-MAX_FILENAME_LENGTH..] || filename.to_s
  ::Dir::Tmpname.create([limited_filename, PNG_EXTENSION]) do |tmp_image_filename|
    image.write_to_file(tmp_image_filename)
    FileUtils.mv(tmp_image_filename, filename)
  end
end