class Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver

Attributes

new_file_name[R]
old_file_name[R]

Public Class Methods

new(new_file_name, old_file_name = nil, **options) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 16
def initialize(new_file_name, old_file_name = nil, **options)
  @new_file_name = new_file_name
  @old_file_name = old_file_name || "#{new_file_name}~"

  @color_distance_limit = options[:color_distance_limit]
  @shift_distance_limit = options[:shift_distance_limit]
  @skip_area = options[:skip_area]

  reset
end

Public Instance Methods

add_black_box(image, _region) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 44
def add_black_box(image, _region)
  image
end
adds_error_details_to(log) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 107
def adds_error_details_to(log)
  max_color_distance = self.max_color_distance.ceil(1)
  max_shift_distance = self.max_shift_distance

  log[:max_color_distance] = max_color_distance
  log.merge!(max_shift_distance: max_shift_distance) if max_shift_distance
end
calculate_max_color_distance(new_image, old_image) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 139
def calculate_max_color_distance(new_image, old_image)
  pixel_pairs = old_image.pixels.zip(new_image.pixels)
  @max_color_distance = pixel_pairs.inject(0) { |max, (p1, p2)|
    next max unless p1 && p2

    d = ChunkyPNG::Color.euclidean_distance_rgba(p1, p2)
    [max, d].max
  }
end
calculate_max_shift_limit(new_img, old_img) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 149
def calculate_max_shift_limit(new_img, old_img)
  (0...new_img.width).each do |x|
    (0...new_img.height).each do |y|
      shift_distance =
        shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
      if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
        @max_shift_distance = shift_distance
        return if @max_shift_distance == Float::INFINITY # rubocop: disable Lint/NonLocalExitFromIterator
      end
    end
  end
end
calculate_metrics() click to toggle source

private

# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 125
def calculate_metrics
  old_file, new_file = load_image_files(@old_file_name, @new_file_name)

  if old_file == new_file
    @max_color_distance = 0
    @max_shift_distance = 0
    return
  end

  old_image, new_image = _load_images(old_file, new_file)
  calculate_max_color_distance(new_image, old_image)
  calculate_max_shift_limit(new_image, old_image)
end
crop(dimensions, i) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 115
def crop(dimensions, i)
  i.crop(0, 0, *dimensions)
end
difference_level(_diff_mask, old_img, region) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 48
def difference_level(_diff_mask, old_img, region)
  size(region).to_f / image_area_size(old_img)
end
dimension_changed?(old_image, new_image) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 176
def dimension_changed?(old_image, new_image)
  return unless old_image.dimension != new_image.dimension

  change_msg = [old_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(" => ")
  warn "Image size has changed for #{@new_file_name}: #{change_msg}"
  true
end
draw_rectangles(images, (left, top, right, bottom), (r, g, b)) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 184
def draw_rectangles(images, (left, top, right, bottom), (r, g, b))
  images.map do |image|
    new_img = image.dup
    new_img.rect(left - 1, top - 1, right + 1, bottom + 1, ChunkyPNG::Color.rgb(r, g, b))
    new_img
  end
end
filter_image_with_median(_image) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 40
def filter_image_with_median(_image)
  raise NotImplementedError
end
find_difference_region(new_image, old_image, color_distance_limit, shift_distance_limit, area_size_limit, fast_fail: false) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 66
def find_difference_region(new_image, old_image, color_distance_limit, shift_distance_limit, area_size_limit, fast_fail: false)
  return nil, nil if new_image.pixels == old_image.pixels

  if fast_fail && !(color_distance_limit || shift_distance_limit || area_size_limit)
    return [0, 0, width_for(new_image), height_for(new_image)], nil
  end

  region = find_top(old_image, new_image)
  region = if region.nil? || region[1].nil?
    nil
  else
    find_diff_rectangle(old_image, new_image, region)
  end

  [region, nil]
end
from_file(filename) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 119
def from_file(filename)
  ChunkyPNG::Image.from_file(filename)
end
height_for(image) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 83
def height_for(image)
  image.height
end
image_area_size(old_img) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 52
def image_area_size(old_img)
  width_for(old_img) * height_for(old_img)
end
load_image_files(old_file_name, file_name) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 170
def load_image_files(old_file_name, file_name)
  old_file = File.binread(old_file_name)
  new_file = File.binread(file_name)
  [old_file, new_file]
end
load_images(old_file_name, new_file_name) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 34
def load_images(old_file_name, new_file_name)
  old_bytes, new_bytes = load_image_files(old_file_name, new_file_name)

  _load_images(old_bytes, new_bytes)
end
max_color_distance() click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 97
def max_color_distance
  calculate_metrics unless @max_color_distance
  @max_color_distance
end
max_shift_distance() click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 102
def max_shift_distance
  calculate_metrics unless @max_shift_distance || !@shift_distance_limit
  @max_shift_distance
end
reset() click to toggle source

Resets the calculated data about the comparison with regard to the “new_image”. Data about the original image is kept.

# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 29
def reset
  @max_color_distance = @color_distance_limit ? 0 : nil
  @max_shift_distance = @shift_distance_limit ? 0 : nil
end
resize_image_to(image, new_width, new_height) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 166
def resize_image_to(image, new_width, new_height)
  image.resample_bilinear(new_width, new_height)
end
save_image_to(image, filename) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 162
def save_image_to(image, filename)
  image.save(filename)
end
shift_distance_different?() click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 61
def shift_distance_different?
  # Stub
  true
end
shift_distance_equal?() click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 56
def shift_distance_equal?
  # Stub
  false
end
size(region) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 91
def size(region)
  return 0 unless region

  (region[2] - region[0] + 1) * (region[3] - region[1] + 1)
end
width_for(image) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 87
def width_for(image)
  image.width
end

Private Instance Methods

_load_images(old_file, new_file) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 348
def _load_images(old_file, new_file)
  [ChunkyPNG::Image.from_blob(old_file), ChunkyPNG::Image.from_blob(new_file)]
end
color_distance_at(new_img, old_img, x, y, shift_distance_limit:) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 267
def color_distance_at(new_img, old_img, x, y, shift_distance_limit:)
  org_color = old_img[x, y]
  if shift_distance_limit
    start_x = [0, x - shift_distance_limit].max
    end_x = [x + shift_distance_limit, new_img.width - 1].min
    xs = (start_x..end_x).to_a
    start_y = [0, y - shift_distance_limit].max
    end_y = [y + shift_distance_limit, new_img.height - 1].min
    ys = (start_y..end_y).to_a
    new_pixels = xs.product(ys)
    distances = new_pixels.map { |dx, dy|
      new_color = new_img[dx, dy]
      ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
    }
    distances.min
  else
    ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
  end
end
color_matches(new_img, org_color, x, y, color_distance_limit) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 340
def color_matches(new_img, org_color, x, y, color_distance_limit)
  new_color = new_img[x, y]
  return new_color == org_color unless color_distance_limit

  color_distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
  color_distance <= color_distance_limit
end
find_bottom(old_img, new_img, left, right, bottom) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 234
def find_bottom(old_img, new_img, left, right, bottom)
  if bottom
    (old_img.height - 1).step(bottom + 1, -1).find do |y|
      (left..right).find do |x|
        bottom = y unless same_color?(old_img, new_img, x, y)
      end
    end
  end
  bottom
end
find_diff_rectangle(org_img, new_img, region) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 194
def find_diff_rectangle(org_img, new_img, region)
  left, top, right, bottom = find_left_right_and_top(org_img, new_img, region)
  bottom = find_bottom(org_img, new_img, left, right, bottom)
  [left, top, right, bottom]
end
find_left_right_and_top(old_img, new_img, region) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 209
def find_left_right_and_top(old_img, new_img, region)
  left = region[0] || old_img.width - 1
  top = region[1]
  right = region[2] || 0
  bottom = region[3]
  old_img.height.times do |y|
    (0...left).find do |x|
      next if same_color?(old_img, new_img, x, y)

      top ||= y
      bottom = y
      left = x
      right = x if x > right
      x
    end
    (old_img.width - 1).step(right + 1, -1).find do |x|
      unless same_color?(old_img, new_img, x, y)
        bottom = y
        right = x
      end
    end
  end
  [left, top, right, bottom]
end
find_top(old_img, new_img) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 200
def find_top(old_img, new_img)
  old_img.height.times do |y|
    old_img.width.times do |x|
      return [x, y, x, y] unless same_color?(old_img, new_img, x, y)
    end
  end
  nil
end
same_color?(old_img, new_img, x, y) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 245
def same_color?(old_img, new_img, x, y)
  @skip_area&.each do |skip_start_x, skip_start_y, skip_end_x, skip_end_y|
    return true if skip_start_x <= x && x <= skip_end_x && skip_start_y <= y && y <= skip_end_y
  end

  color_distance =
    color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit)
  if !@max_color_distance || color_distance > @max_color_distance
    @max_color_distance = color_distance
  end
  color_matches = color_distance == 0 || (@color_distance_limit && @color_distance_limit > 0 &&
    color_distance <= @color_distance_limit)
  return color_matches if !@shift_distance_limit || @max_shift_distance == Float::INFINITY

  shift_distance = (color_matches && 0) ||
    shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
  if shift_distance && (@max_shift_distance.nil? || shift_distance > @max_shift_distance)
    @max_shift_distance = shift_distance
  end
  color_matches
end
shift_distance_at(new_img, old_img, x, y, color_distance_limit:) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 287
def shift_distance_at(new_img, old_img, x, y, color_distance_limit:)
  org_color = old_img[x, y]
  shift_distance = 0
  loop do
    bounds_breached = 0
    top_row = y - shift_distance
    if top_row >= 0 # top
      ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
        if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
          return shift_distance
        end
      end
    else
      bounds_breached += 1
    end
    if shift_distance > 0
      if (x - shift_distance) >= 0 # left
        ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
          .each do |dy|
          if color_matches(new_img, org_color, x - shift_distance, dy, color_distance_limit)
            return shift_distance
          end
        end
      else
        bounds_breached += 1
      end
      if (y + shift_distance) < new_img.height # bottom
        ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
          if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
            return shift_distance
          end
        end
      else
        bounds_breached += 1
      end
      if (x + shift_distance) < new_img.width # right
        ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
          .each do |dy|
          if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
            return shift_distance
          end
        end
      else
        bounds_breached += 1
      end
    end
    break if bounds_breached == 4

    shift_distance += 1
  end
  Float::INFINITY
end