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

Attributes

color_distance_limit[RW]
shift_distance_limit[RW]
skip_area[RW]

Public Class Methods

new(comparison, driver = nil) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 84
def initialize(comparison, driver = nil)
  @comparison = comparison
  @driver = driver

  @color_distance_limit = comparison.options[:color_distance_limit]
  @shift_distance_limit = comparison.options[:shift_distance_limit]
  @skip_area = comparison.options[:skip_area]
end

Public Instance Methods

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 218
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 do |dx, dy|
      ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[dx, dy])
    end
    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 291
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
difference_level(_diff_mask, base_image, region) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 124
def difference_level(_diff_mask, base_image, region)
  image_area_size = @driver.image_area_size(base_image)
  return nil if image_area_size.zero?

  region.size.to_f / image_area_size
end
find_bottom(old_img, new_img, left, right, bottom, cache:) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 176
def find_bottom(old_img, new_img, left, right, bottom, cache:)
  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, cache: cache)
      end
    end
  end

  bottom
end
find_diff_rectangle(org_img, new_img, area_coordinates, cache:) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 131
def find_diff_rectangle(org_img, new_img, area_coordinates, cache:)
  left, top, right, bottom = find_left_right_and_top(org_img, new_img, area_coordinates, cache: cache)
  bottom = find_bottom(org_img, new_img, left, right, bottom, cache: cache)

  Region.from_edge_coordinates(left, top, right, bottom)
end
find_difference_region(comparison) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 97
def find_difference_region(comparison)
  new_image, base_image, = comparison.new_image, comparison.base_image

  meta = {}
  meta[:max_color_distance] = 0
  meta[:max_shift_distance] = 0 if shift_distance_limit

  region = find_top(base_image, new_image, cache: meta)
  region = if region.nil? || region[1].nil?
    nil
  else
    find_diff_rectangle(base_image, new_image, region, cache: meta)
  end

  result = Difference.new(region, meta, comparison)

  unless result.blank?
    meta[:max_color_distance] = meta[:max_color_distance].ceil(1) if meta[:max_color_distance]

    if comparison.options[:tolerance]
      meta[:difference_level] = difference_level(nil, base_image, region)
    end
  end

  result
end
find_left_right_and_top(old_img, new_img, region, cache:) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 147
def find_left_right_and_top(old_img, new_img, region, cache:)
  region = region.is_a?(Region) ? region.to_edge_coordinates : 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, cache: cache)

      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, cache: cache)
        bottom = y
        right = x
      end
    end
  end

  [left, top, right, bottom]
end
find_top(old_img, new_img, cache:) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 138
def find_top(old_img, new_img, cache:)
  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, cache: cache)
    end
  end
  nil
end
perform() click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 93
def perform
  find_difference_region(@comparison)
end
same_color?(old_img, new_img, x, y, cache:) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 188
def same_color?(old_img, new_img, x, y, cache:)
  return true if skipped_region?(x, y)

  color_distance =
    color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit)

  if color_distance > cache[:max_color_distance]
    cache[: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 || cache[: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 && (cache[:max_shift_distance].nil? || shift_distance > cache[:max_shift_distance])
    cache[: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 238
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
skipped_region?(x, y) click to toggle source
# File lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb, line 212
def skipped_region?(x, y)
  return false unless @skip_area

  @skip_area.any? { |region| region.cover?(x, y) }
end