lib/capybara/screenshot/diff/image_compare.rb in capybara-screenshot-diff-1.7.1 vs lib/capybara/screenshot/diff/image_compare.rb in capybara-screenshot-diff-1.8.0
- old
+ new
@@ -3,255 +3,226 @@
module Capybara
module Screenshot
module Diff
LOADED_DRIVERS = {}
- # Compare two images and determine if they are equal, different, or within some comparison
+ # Compare two image and determine if they are equal, different, or within some comparison
# range considering color values and difference area size.
- class ImageCompare < SimpleDelegator
- TMP_FILE_SUFFIX = "~"
+ class ImageCompare
+ TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
attr_reader :driver, :driver_options
- attr_reader :annotated_new_file_name, :annotated_old_file_name, :new_file_name, :old_file_name, :skip_area
- attr_accessor :shift_distance_limit, :area_size_limit, :color_distance_limit
+ attr_reader :annotated_image_path, :annotated_base_image_path,
+ :image_path, :base_image_path,
+ :new_file_name, :old_file_name
- def initialize(new_file_name, old_file_name = nil, options = {})
- options = old_file_name if old_file_name.is_a?(Hash)
+ def initialize(image_path, base_image_path, options = {})
+ @image_path = Pathname.new(image_path)
- @new_file_name = new_file_name
- @old_file_name = old_file_name || "#{new_file_name}#{ImageCompare::TMP_FILE_SUFFIX}"
- @annotated_old_file_name = "#{new_file_name.chomp(".png")}.committed.png"
- @annotated_new_file_name = "#{new_file_name.chomp(".png")}.latest.png"
+ @new_file_name = @image_path.to_s
+ @annotated_image_path = @image_path.sub_ext(".diff.png")
- @driver_options = options
+ @base_image_path = Pathname.new(base_image_path)
- @color_distance_limit = options[:color_distance_limit] || 0
- @area_size_limit = options[:area_size_limit]
- @shift_distance_limit = options[:shift_distance_limit]
- @dimensions = options[:dimensions]
- @skip_area = options[:skip_area]
- @tolerance = options[:tolerance]
- @median_filter_window_size = options[:median_filter_window_size]
+ @old_file_name = @base_image_path.to_s
+ @annotated_base_image_path = @base_image_path.sub_ext(".diff.png")
- driver_klass = find_driver_class_for(@driver_options.fetch(:driver, :chunky_png))
- @driver = driver_klass.new(@new_file_name, @old_file_name, **@driver_options)
+ @driver_options = options.dup
- super(@driver)
+ @driver = Drivers.for(@driver_options)
end
- def skip_area=(new_skip_area)
- @skip_area = new_skip_area
- driver.skip_area = @skip_area
- 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?
- return false unless old_file_exists?
+ @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
return true if new_file_size == old_file_size
- images = driver.load_images(@old_file_name, @new_file_name)
- old_image, new_image = preprocess_images(images, driver)
+ comparison = load_and_process_images
- return false if driver.dimension_changed?(old_image, new_image)
+ unless driver.same_dimension?(comparison)
+ @error_message = build_error_for_different_dimensions(comparison)
+ return false
+ end
- self.difference_region, meta = driver.find_difference_region(
- new_image,
- old_image,
- @color_distance_limit,
- @shift_distance_limit,
- @area_size_limit,
- fast_fail: true
- )
+ return true if driver.same_pixels?(comparison)
- return true if difference_region_area_size.zero? || difference_region_empty?(new_image, difference_region)
- return true if @area_size_limit && difference_region_area_size <= @area_size_limit
- return true if @tolerance && @tolerance >= driver.difference_level(meta, old_image, difference_region)
- # TODO: Remove this or find similar solution for vips
- return true if @shift_distance_limit && driver.shift_distance_equal?
+ # 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?
+
+ @error_message = @difference.inspect
false
end
- # Compare the two images referenced by this object, and return `true` if they are different,
+ # Compare the two image referenced by this object, and return `true` if they are different,
# and `false` if they are the same.
def different?
- return false unless old_file_exists?
+ @error_message = nil
- images = driver.load_images(@old_file_name, @new_file_name)
- old_image, new_image = preprocess_images(images, driver)
+ @error_message = _different?
- if driver.dimension_changed?(old_image, new_image)
- self.difference_region = Region.from_edge_coordinates(
- 0,
- 0,
- [driver.width_for(old_image), driver.width_for(new_image)].min,
- [driver.height_for(old_image), driver.height_for(new_image)].min
- )
+ clean_tmp_files unless @error_message
- return different(*images)
- end
+ !@error_message.nil?
+ end
- self.difference_region, meta = driver.find_difference_region(
- new_image,
- old_image,
- @color_distance_limit,
- @shift_distance_limit,
- @area_size_limit
- )
+ def build_error_for_different_dimensions(comparison)
+ change_msg = [comparison.base_image, comparison.new_image]
+ .map { |i| driver.dimension(i).join("x") }
+ .join(" => ")
- return not_different if difference_region_area_size.zero? || difference_region_empty?(old_image, difference_region)
- return not_different if @area_size_limit && difference_region_area_size <= @area_size_limit
- return not_different if @tolerance && @tolerance > driver.difference_level(meta, old_image, difference_region)
- # TODO: Remove this or find similar solution for vips
- return not_different if @shift_distance_limit && !driver.shift_distance_different?
-
- different(*images)
+ "Screenshot dimension has been changed for #{@new_file_name}: #{change_msg}"
end
def clean_tmp_files
- FileUtils.cp @old_file_name, @new_file_name if old_file_exists?
- File.delete(@old_file_name) if old_file_exists?
- File.delete(@annotated_old_file_name) if File.exist?(@annotated_old_file_name)
- File.delete(@annotated_new_file_name) if File.exist?(@annotated_new_file_name)
+ @annotated_base_image_path.unlink if @annotated_base_image_path.exist?
+ @annotated_image_path.unlink if @annotated_image_path.exist?
end
- def save(old_img, new_img, annotated_old_file_name, annotated_new_file_name)
- driver.save_image_to(old_img, annotated_old_file_name)
- driver.save_image_to(new_img, annotated_new_file_name)
+ def save(image, image_path)
+ driver.save_image_to(image, image_path.to_s)
end
- def old_file_exists?
- @old_file_name && File.exist?(@old_file_name)
+ def image_files_exist?
+ @base_image_path.exist? && @image_path.exist?
end
- def reset
- self.difference_region = nil
- driver.reset
+ NEW_LINE = "\n"
+
+ attr_reader :error_message
+
+ private
+
+ def without_tolerable_options?
+ (@driver_options.keys & TOLERABLE_OPTIONS).empty?
end
- NEW_LINE = "\n"
+ 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?
- def error_message
- result = {
- area_size: difference_region_area_size,
- region: difference_coordinates
- }
+ comparison = load_and_process_images
- driver.adds_error_details_to(result)
+ 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)
+ end
+
+ def load_and_process_images
+ images = driver.load_images(old_file_name, new_file_name)
+ base_image, new_image = preprocess_images(images)
+ Comparison.new(new_image, base_image, @driver_options)
+ end
+
+ def build_error_message(difference)
[
- "(#{result.to_json})",
+ "(#{difference.inspect})",
new_file_name,
- annotated_old_file_name,
- annotated_new_file_name
+ annotated_base_image_path.to_path,
+ annotated_image_path.to_path
].join(NEW_LINE)
end
- def difference_coordinates
- difference_region&.to_edge_coordinates
+ def skip_area
+ @driver_options[:skip_area]
end
- def difference_region_area_size
- return 0 unless difference_region
-
- difference_region.size
+ def median_filter_window_size
+ @driver_options[:median_filter_window_size]
end
- private
-
- attr_accessor :difference_region
-
- def different(old_image, new_image)
- annotate_and_save([old_image, new_image], difference_region)
- true
+ def dimensions
+ @driver_options[:dimensions]
end
- def find_driver_class_for(driver)
- driver = AVAILABLE_DRIVERS.first if driver == :auto
-
- LOADED_DRIVERS[driver] ||=
- case driver
- when :chunky_png
- require "capybara/screenshot/diff/drivers/chunky_png_driver"
- Drivers::ChunkyPNGDriver
- when :vips
- require "capybara/screenshot/diff/drivers/vips_driver"
- Drivers::VipsDriver
- else
- fail "Wrong adapter #{driver.inspect}. Available adapters: #{AVAILABLE_DRIVERS.inspect}"
- end
+ def different(difference)
+ annotate_and_save_images(difference)
+ build_error_message(difference)
end
- def preprocess_images(images, driver = self)
- old_img = preprocess_image(images.first, driver)
- new_img = preprocess_image(images.last, driver)
-
- [old_img, new_img]
+ def preprocess_images(images)
+ images.map { |image| preprocess_image(image) }
end
- def preprocess_image(image, driver = self)
+ def preprocess_image(image)
result = image
- if @dimensions && driver.inscribed?(@dimensions, result)
- result = driver.crop(@dimensions, result)
+ # 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 @median_filter_window_size
- result = driver.filter_image_with_median(image, @median_filter_window_size)
+ if skip_area
+ result = ignore_skipped_area(result)
end
- if @skip_area
- result = @skip_area.reduce(result) { |image, region| driver.add_black_box(image, region) }
+ if median_filter_window_size
+ result = blur_image_by(image, median_filter_window_size)
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) }
+ end
+
def old_file_size
- @old_file_size ||= old_file_exists? && File.size(@old_file_name)
+ @old_file_size ||= image_files_exist? && File.size(@old_file_name)
end
def new_file_size
File.size(@new_file_name)
end
def not_different
- clean_tmp_files
- false
+ nil
end
- def difference_region_empty?(new_image, region)
- region.nil? ||
- (
- region.height == height_for(new_image) &&
- region.width == width_for(new_image) &&
- region.x.zero? &&
- region.y.zero?
- )
+ 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)
end
- def annotate_and_save(images, region)
- annotated_images = annotate_difference(images, region)
- annotated_images = annotate_skip_areas(annotated_images, @skip_area) if @skip_area
-
- save(*annotated_images, @annotated_old_file_name, @annotated_new_file_name)
+ 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(images, region)
- driver.draw_rectangles(images, region, DIFF_COLOR)
+ 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(annotated_images, skip_areas)
- skip_areas.reduce(annotated_images) do |annotated_images, region|
- driver.draw_rectangles(annotated_images, region, SKIP_COLOR)
+ 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