lib/capybara/screenshot/diff/image_compare.rb in capybara-screenshot-diff-0.10.2 vs lib/capybara/screenshot/diff/image_compare.rb in capybara-screenshot-diff-0.11.0

- old
+ new

@@ -10,14 +10,15 @@ attr_reader :annotated_new_file_name, :annotated_old_file_name, :new_file_name, :old_file_name def initialize(new_file_name, old_file_name = nil, dimensions: nil, color_distance_limit: nil, - area_size_limit: nil) + area_size_limit: nil, shift_distance_limit: nil) @new_file_name = new_file_name @color_distance_limit = color_distance_limit @area_size_limit = area_size_limit + @shift_distance_limit = shift_distance_limit @dimensions = dimensions @old_file_name = old_file_name || "#{new_file_name}~" @annotated_old_file_name = "#{new_file_name.chomp('.png')}_0.png~" @annotated_new_file_name = "#{new_file_name.chomp('.png')}_1.png~" reset @@ -25,10 +26,11 @@ # Resets the calculated data about the comparison with regard to the "new_image". # Data about the original image is kept. def reset @max_color_distance = @color_distance_limit ? 0 : nil + @max_shift_distance = @shift_distance_limit ? 0 : nil @left = @top = @right = @bottom = nil end # Compare the two image files and return `true` or `false` as quickly as possible. # Return falsish if the old file does not exist or the image dimensions do not match. @@ -98,21 +100,16 @@ save_images(@annotated_new_file_name, annotated_new_img, @annotated_old_file_name, annotated_old_img) true end - private def not_different - clean_tmp_files - false - end - def old_file_exists? @old_file_name && File.exist?(@old_file_name) end def old_file_size - @_old_filesize ||= old_file_exists? && File.size(@old_file_name) + @old_file_size ||= old_file_exists? && File.size(@old_file_name) end def new_file_size File.size(@new_file_name) end @@ -124,24 +121,43 @@ def size (@right - @left + 1) * (@bottom - @top + 1) end def max_color_distance - return @max_color_distance if @max_color_distance + calculate_metrics unless @max_color_distance + @max_color_distance + end + + def max_shift_distance + calculate_metrics unless @max_shift_distance + @max_shift_distance + end + + private + + def calculate_metrics old_file, new_file = load_image_files(@old_file_name, @new_file_name) - return @max_color_distance = 0 if old_file == new_file + @max_color_distance = 0 if old_file == new_file old_image, new_image = load_images(old_file, new_file) pixel_pairs = old_image.pixels.zip(new_image.pixels) @max_color_distance = pixel_pairs.inject(0) do |max, (p1, p2)| d = ChunkyPNG::Color.euclidean_distance_rgba(p1, p2) [max, d].max end + + (0...new_image.width).each do |_x| + (0...new_image.height).each do |y| + end + end end - private + def not_different + clean_tmp_files + false + end def save_images(new_file_name, new_img, org_file_name, org_img) org_img.save(org_file_name) new_img.save(new_file_name) end @@ -168,43 +184,43 @@ change_msg = [org_image, new_image].map { |i| "#{i.width}x#{i.height}" }.join(' => ') puts "Image size has changed for #{@new_file_name}: #{change_msg}" true end - private def crop_images(images, dimensions) + def crop_images(images, dimensions) images.map! do |i| if i.dimension.to_a == dimensions || i.width < dimensions[0] || i.height < dimensions[1] i else i.crop(0, 0, *dimensions) end end end - private def draw_rectangles(images, bottom, left, right, top) + def draw_rectangles(images, bottom, left, right, top) images.map do |image| new_img = image.dup new_img.rect(left - 1, top - 1, right + 1, bottom + 1, ChunkyPNG::Color.rgb(255, 0, 0)) new_img end end - private def find_diff_rectangle(org_img, new_img) + def find_diff_rectangle(org_img, new_img) left, top, right, bottom = find_left_right_and_top(org_img, new_img) bottom = find_bottom(org_img, new_img, left, right, bottom) [left, top, right, bottom] end - private def find_top(old_img, new_img) + 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 end - private def find_left_right_and_top(old_img, new_img) + def find_left_right_and_top(old_img, new_img) top = @top bottom = @bottom left = @left || old_img.width - 1 right = @right || 0 old_img.height.times do |y| @@ -224,29 +240,109 @@ end end [left, top, right, bottom] end - private def find_bottom(old_img, new_img, left, right, bottom) + 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 - private def same_color?(old_img, new_img, x, y) + def same_color?(old_img, new_img, x, y) + color_distance = + color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit) + shift_distance = + shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit) + if !@max_color_distance || color_distance > @max_color_distance + @max_color_distance = color_distance + end + if !@max_shift_distance || shift_distance > @max_shift_distance + @max_shift_distance = shift_distance + end + (color_distance == 0 || (@color_distance_limit && @color_distance_limit > 0 && + color_distance <= @color_distance_limit)) && + (shift_distance == 0 || (@shift_distance_limit && @shift_distance_limit > 0 && + shift_distance <= @shift_distance_limit)) + end + + def color_distance_at(new_img, old_img, x, y, shift_distance_limit:) org_color = old_img[x, y] - new_color = new_img[x, y] - return true if org_color == new_color + 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| + new_color = new_img[dx, dy] + ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color) + end + distances.min + else + new_color = new_img[x, y] + ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color) + end + end - distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color) - @max_color_distance = distance if !@max_color_distance || distance > @max_color_distance + def shift_distance_at(new_img, old_img, x, y, color_distance_limit:) + org_color = old_img[x, y] + shift_distance = 0 + loop do + if (y - shift_distance) >= 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, y - shift_distance, color_distance_limit) + return shift_distance + end + end + end + if shift_distance > 0 + if (x - shift_distance) >= 0 # left + ([0, y - shift_distance + 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 + 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 + end + if (x + shift_distance) < new_img.width # right + ([0, y - shift_distance + 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 + end + end + shift_distance += 1 + # puts "Bumped shift_distance to #{shift_distance}" + end + shift_distance + end - @color_distance_limit && @color_distance_limit > 0 && distance <= @color_distance_limit + def color_matches(new_img, org_color, dx, dy, color_distance_limit) + new_color = new_img[dx, dy] + return new_color == org_color unless color_distance_limit + color_distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color) + # if color_distance > 0 + # puts "color_distance: #{dx} #{dy} #{color_distance} #{color_distance_limit} " \ + # "#{color_distance <= color_distance_limit}" + # end + color_distance <= color_distance_limit end end end end end