lib/capybara/screenshot/diff/image_compare.rb in capybara-screenshot-diff-1.8.3 vs lib/capybara/screenshot/diff/image_compare.rb in capybara-screenshot-diff-1.9.0

- old
+ new

@@ -1,7 +1,9 @@ # frozen_string_literal: true +require "capybara/screenshot/diff/comparison" + module Capybara module Screenshot module Diff LOADED_DRIVERS = {} @@ -9,220 +11,184 @@ # range considering color values and difference area size. class ImageCompare TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze attr_reader :driver, :driver_options + attr_reader :image_path, :base_image_path + attr_reader :difference, :error_message - attr_reader :annotated_image_path, :annotated_base_image_path, - :image_path, :base_image_path, - :new_file_name, :old_file_name - def initialize(image_path, base_image_path, options = {}) @image_path = Pathname.new(image_path) - - @new_file_name = @image_path.to_s - @annotated_image_path = @image_path.sub_ext(".diff.png") - @base_image_path = Pathname.new(base_image_path) - @old_file_name = @base_image_path.to_s - @annotated_base_image_path = @base_image_path.sub_ext(".diff.png") - @driver_options = options.dup @driver = Drivers.for(@driver_options) end # Compare the two image files and return `true` or `false` as quickly as possible. # Return falsely if the old file does not exist or the image dimensions do not match. def quick_equal? - @error_message = nil - return false unless image_files_exist? - # TODO: Confirm this change. There are screenshots with the same size, but there is a big difference + require_images_exists! + + # NOTE: This is very fuzzy logic, but so far it's helps to support current performance. return true if new_file_size == old_file_size comparison = load_and_process_images unless driver.same_dimension?(comparison) - @error_message = build_error_for_different_dimensions(comparison) + self.difference = build_failed_difference(comparison, {different_dimensions: true}) return false end - return true if driver.same_pixels?(comparison) + if driver.same_pixels?(comparison) + self.difference = build_no_difference(comparison) + return true + end - # Could not make any difference to be tolerable, so skip and return as not equal + # NOTE: Could not make any difference to be tolerable, so skip and return as not equal. return false if without_tolerable_options? - @difference = driver.find_difference_region(comparison) - return true unless @difference.different? + self.difference = driver.find_difference_region(comparison) - @error_message = @difference.inspect - false + !difference.different? end # Compare the two image referenced by this object, and return `true` if they are different, # and `false` if they are the same. def different? - @error_message = nil + processed.difference.different? + end - @error_message = _different? + def reporter + @reporter ||= begin + current_difference = difference || build_no_difference(nil) + Capybara::Screenshot::Diff::Reporters::Default.new(current_difference) + end + end - clean_tmp_files unless @error_message + def processed? + !!difference + end - !@error_message.nil? + def processed + self.difference = find_difference unless processed? + @error_message ||= reporter.generate + self end - def build_error_for_different_dimensions(comparison) - change_msg = [comparison.base_image, comparison.new_image] - .map { |i| driver.dimension(i).join("x") } - .join(" => ") + private - "Screenshot dimension has been changed for #{@new_file_name}: #{change_msg}" + def find_difference + require_images_exists! + + comparison = load_and_process_images + + unless driver.same_dimension?(comparison) + return build_failed_difference(comparison, {different_dimensions: true}) + end + + if driver.same_pixels?(comparison) + build_no_difference(comparison) + else + driver.find_difference_region(comparison) + end end - def clean_tmp_files - @annotated_base_image_path.unlink if @annotated_base_image_path.exist? - @annotated_image_path.unlink if @annotated_image_path.exist? + def require_images_exists! + raise ArgumentError, "There is no original (base) screenshot version to compare, located: #{base_image_path}" unless base_image_path.exist? + raise ArgumentError, "There is no new screenshot version to compare, located: #{image_path}" unless image_path.exist? end - def save(image, image_path) - driver.save_image_to(image, image_path.to_s) + def difference=(new_difference) + @error_message = nil + @reporter = nil + @difference = new_difference end def image_files_exist? @base_image_path.exist? && @image_path.exist? end - NEW_LINE = "\n" - - attr_reader :error_message - - private - def without_tolerable_options? (@driver_options.keys & TOLERABLE_OPTIONS).empty? end - def _different? - raise "There is no original (base) screenshot version to compare, located: #{@base_image_path}" unless @base_image_path.exist? - raise "There is no new screenshot version to compare, located: #{@image_path}" unless @image_path.exist? - - comparison = load_and_process_images - - unless driver.same_dimension?(comparison) - return build_error_for_different_dimensions(comparison) - end - - return not_different if driver.same_pixels?(comparison) - - @difference = driver.find_difference_region(comparison) - return not_different unless @difference.different? - - different(@difference) + def build_failed_difference(comparison, failed_by) + Difference.new( + nil, + {difference_level: nil, max_color_distance: 0}, + comparison, + failed_by + ) end def load_and_process_images - images = driver.load_images(old_file_name, new_file_name) + images = driver.load_images(base_image_path, image_path) base_image, new_image = preprocess_images(images) - Comparison.new(new_image, base_image, @driver_options) + Comparison.new(new_image, base_image, @driver_options, driver, image_path, base_image_path) end - def build_error_message(difference) - [ - "(#{difference.inspect})", - new_file_name, - annotated_base_image_path.to_path, - annotated_image_path.to_path - ].join(NEW_LINE) - end - def skip_area @driver_options[:skip_area] end def median_filter_window_size @driver_options[:median_filter_window_size] end - def dimensions - @driver_options[:dimensions] - end - - def different(difference) - annotate_and_save_images(difference) - build_error_message(difference) - end - def preprocess_images(images) images.map { |image| preprocess_image(image) } end def preprocess_image(image) result = image - # FIXME: How can we access to this method from public interface? Is this not documented feature? - if dimensions && driver.inscribed?(dimensions, result) - result = driver.crop(dimensions, result) - end - if skip_area result = ignore_skipped_area(result) end if median_filter_window_size - result = blur_image_by(image, median_filter_window_size) + if driver.is_a?(Drivers::VipsDriver) + result = blur_image_by(image, median_filter_window_size) + else + warn( + "[capybara-screenshot-diff] Median filter has been skipped for #{image_path} " \ + "because it is not supported by #{driver.class.name}" + ) + end end result end def blur_image_by(image, size) driver.filter_image_with_median(image, size) end def ignore_skipped_area(image) - skip_area.reduce(image) { |memo, region| driver.add_black_box(memo, region) } + skip_area&.reduce(image) { |memo, region| driver.add_black_box(memo, region) } end def old_file_size - @old_file_size ||= image_files_exist? && File.size(@old_file_name) + base_image_path.size end def new_file_size - File.size(@new_file_name) + image_path.size end - def not_different - nil + def build_no_difference(comparison = nil) + Difference.new( + nil, + {difference_level: nil, max_color_distance: 0}, + comparison || build_comparison + ).freeze end - def annotate_and_save_images(difference) - annotate_and_save_image(difference, difference.comparison.new_image, @annotated_image_path) - annotate_and_save_image(difference, difference.comparison.base_image, @annotated_base_image_path) + def build_comparison + Capybara::Screenshot::Diff::Comparison.new(nil, nil, driver_options, driver, image_path, base_image_path).freeze end - - def annotate_and_save_image(difference, image, image_path) - image = annotate_difference(image, difference.region) - image = annotate_skip_areas(image, difference.skip_area) if difference.skip_area - save(image, image_path.to_path) - end - - DIFF_COLOR = [255, 0, 0, 255].freeze - - def annotate_difference(image, region) - driver.draw_rectangles(Array[image], region, DIFF_COLOR, offset: 1).first - end - - SKIP_COLOR = [255, 192, 0, 255].freeze - - def annotate_skip_areas(image, skip_areas) - skip_areas.reduce(image) do |memo, region| - driver.draw_rectangles(Array[memo], region, SKIP_COLOR).first - end - end - end - - class Comparison < Struct.new(:new_image, :base_image, :options) end end end end