lib/capybara/screenshot/diff/drivers/vips_driver.rb in capybara-screenshot-diff-1.7.1 vs lib/capybara/screenshot/diff/drivers/vips_driver.rb in capybara-screenshot-diff-1.8.0

- old
+ new

@@ -5,186 +5,136 @@ rescue LoadError => e warn 'Required ruby-vips gem is missing. Add `gem "ruby-vips"` to Gemfile' if e.message.include?("vips") raise end -require_relative "./chunky_png_driver" +require "capybara/screenshot/diff/drivers/base_driver" module Capybara module Screenshot module Diff # Compare two images and determine if they are equal, different, or within some comparison # range considering color values and difference area size. module Drivers - class VipsDriver - attr_reader :new_file_name, :old_file_name, :options + class VipsDriver < BaseDriver + def find_difference_region(comparison) + new_image, base_image, options = comparison.new_image, comparison.base_image, comparison.options - def initialize(new_file_name, old_file_name = nil, options = {}) - options = old_file_name if old_file_name.is_a?(Hash) - - @new_file_name = new_file_name - @old_file_name = old_file_name || "#{new_file_name}#{ImageCompare::TMP_FILE_SUFFIX}" - - @options = options || {} - - reset - end - - def skip_area=(new_skip_area) - # noop - end - - # Resets the calculated data about the comparison with regard to the "new_image". - # Data about the original image is kept. - def reset - end - - def shift_distance_equal? - warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \ - "please use median_filter_window_size and color_distance_limit options" - chunky_png_comparator.quick_equal? - end - - def shift_distance_different? - warn "[capybara-screenshot-diff] Instead of shift_distance_limit " \ - "please use median_filter_window_size and color_distance_limit options" - chunky_png_comparator.different? - end - - def find_difference_region(new_image, old_image, color_distance_limit, _shift_distance_limit, _area_size_limit, fast_fail: false) - diff_mask = VipsUtil.difference_mask(color_distance_limit, old_image, new_image) + diff_mask = VipsUtil.difference_mask(base_image, new_image, options[:color_distance_limit]) region = VipsUtil.difference_region_by(diff_mask) + region = nil if region && same_as?(region, base_image) - [region, diff_mask] - end + result = Difference.new(region, {}, comparison) - def adds_error_details_to(_log) - end + unless result.blank? + meta = {} + meta[:difference_level] = difference_level(diff_mask, base_image) if comparison.options[:tolerance] + result.meta = meta + end - # old private - - def inscribed?(dimensions, i) - dimension(i) == dimensions || i.width < dimensions[0] || i.height < dimensions[1] + result end def crop(region, i) - result = i.crop(*region.to_top_left_corner_coordinates) - - # FIXME: Vips is caching operations, and if we ware going to read the same file, he will use cached version for this - # so after we cropped files and stored in the same file, the next load will recover old version instead of cropped - # Workaround to make vips works with cropped versions - Vips.cache_set_max(0) - Vips.vips_cache_set_max(1000) - - result + i.crop(*region.to_top_left_corner_coordinates) + rescue Vips::Error => e + warn( + "[capybara-screenshot-diff] Crop has been failed for " \ + "{ region: #{region.to_top_left_corner_coordinates.inspect}, image: #{dimension(i).join("x")} }" + ) + raise e end def filter_image_with_median(image, median_filter_window_size) image.median(median_filter_window_size) end def add_black_box(memo, region) memo.draw_rect([0, 0, 0, 0], *region.to_top_left_corner_coordinates, fill: true) end - def chunky_png_comparator - @chunky_png_comparator ||= ImageCompare.new( - @new_file_name, - @old_file_name, - **@options.merge(driver: :chunky_png, tolerance: nil, median_filter_window_size: nil) - ) - end - def difference_level(diff_mask, old_img, _region = nil) VipsUtil.difference_area_size_by(diff_mask).to_f / image_area_size(old_img) end - def image_area_size(old_img) - width_for(old_img) * height_for(old_img) - end + MAX_FILENAME_LENGTH = 200 - def height_for(image) - image.height - end - - def width_for(image) - image.width - end - - PNG_EXTENSION = ".png" - # Vips could not work with the same file. Per each process we require to create new file def save_image_to(image, filename) - ::Dir::Tmpname.create([filename, PNG_EXTENSION]) do |tmp_image_filename| + # Dir::Tmpname will happily produce tempfile names that are too long for most unix filesystems, + # which leads to "unix error: File name too long". Apply a limit to avoid this. + limited_filename = filename.to_s[-MAX_FILENAME_LENGTH..] || filename.to_s + ::Dir::Tmpname.create([limited_filename, PNG_EXTENSION]) do |tmp_image_filename| image.write_to_file(tmp_image_filename) FileUtils.mv(tmp_image_filename, filename) end end def resize_image_to(image, new_width, new_height) - image.resize(1.* new_width / new_height) + image.resize(new_width.to_f / new_height) end - def load_images(old_file_name, new_file_name, driver = self) - [driver.from_file(old_file_name), driver.from_file(new_file_name)] + def load_images(old_file_name, new_file_name) + [from_file(old_file_name), from_file(new_file_name)] end def from_file(filename) - result = ::Vips::Image.new_from_file(filename) + result = ::Vips::Image.new_from_file(filename.to_s) - result = result.colourspace("srgb") if result.bands < 3 + result = result.colourspace(:srgb) if result.bands < 3 result = result.bandjoin(255) if result.bands == 3 result end - def dimension_changed?(old_image, new_image) - return false if dimension(old_image) == dimension(new_image) - - 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 - def dimension(image) - [image.width, image.height] + [width_for(image), height_for(image)] end - def draw_rectangles(images, region, rgba) + def draw_rectangles(images, region, rgba, offset: 0) images.map do |image| - image.draw_rect(rgba, region.left - 1, region.top - 1, region.width + 2, region.height + 2) + image.draw_rect(rgba, region.left - offset, region.top - offset, region.width + (offset * 2), region.height + (offset * 2)) end end - class VipsUtil - def self.difference(old_image, new_image, color_distance: 0) - diff_mask = difference_mask(color_distance, new_image, old_image) - difference_region_by(diff_mask).to_edge_coordinates - end + def same_pixels?(comparison) + (comparison.new_image == comparison.base_image).min == 255 + end + private + + def same_as?(region, base_image) + region.x.zero? && + region.y.zero? && + region.height == height_for(base_image) && + region.width == width_for(base_image) + end + + class VipsUtil def self.difference_area(old_image, new_image, color_distance: 0) - difference_mask = difference_mask(color_distance, new_image, old_image) + difference_mask = difference_mask(new_image, old_image, color_distance) difference_area_size_by(difference_mask) end def self.difference_area_size_by(difference_mask) diff_mask = difference_mask == 0 diff_mask.hist_find.to_a[0][0].max end - def self.difference_mask(color_distance, old_image, new_image) - (new_image - old_image).abs > color_distance + def self.difference_mask(base_image, new_image, color_distance = nil) + result = (new_image - base_image).abs + + color_distance ? result > color_distance : result end def self.difference_region_by(diff_mask) columns, rows = diff_mask.bandor.project left = columns.profile[1].min - right = columns.width - columns.flip("horizontal").profile[1].min + right = columns.width - columns.flip(:horizontal).profile[1].min top = rows.profile[0].min - bottom = rows.height - rows.flip("vertical").profile[0].min + bottom = rows.height - rows.flip(:vertical).profile[0].min return nil if right < left || bottom < top Region.from_edge_coordinates(left, top, right, bottom) end