eyes_selenium 2.16.0 → 2.17.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -25,7 +25,7 @@ module Applitools::Selenium
25
25
  end
26
26
 
27
27
  def ==(other)
28
- other.kind_of?(web_element.class) && web_element == other
28
+ other.is_a?(web_element.class) && web_element == other
29
29
  end
30
30
  alias_method :eql?, :==
31
31
 
@@ -41,13 +41,18 @@ module Applitools::Selenium
41
41
 
42
42
  def region
43
43
  point = location
44
- left, top, width, height = point.x, point.y, 0, 0
44
+ left = point.x
45
+ top = point.y
46
+ width = 0
47
+ height = 0
45
48
 
46
49
  begin
47
50
  dimension = size
48
- width, height = dimension.width, dimension.height
49
- rescue
51
+ width = dimension.width
52
+ height = dimension.height
53
+ rescue => e
50
54
  # Not supported on all platforms.
55
+ Applitools::EyesLogger.error("Failed extracting size using JavaScript: (#{e.message})")
51
56
  end
52
57
 
53
58
  if left < 0
@@ -60,7 +65,7 @@ module Applitools::Selenium
60
65
  top = 0
61
66
  end
62
67
 
63
- return Applitools::Base::Region.new(left, top, width, height)
68
+ Applitools::Base::Region.new(left, top, width, height)
64
69
  end
65
70
  end
66
71
  end
@@ -10,7 +10,7 @@ module Applitools::Selenium
10
10
  @screenshot = screenshot
11
11
  end
12
12
 
13
- # IMPORTANT This method returns a hash WITHOUT the screenshot property. This is on purspose! The screenshot should
13
+ # IMPORTANT This method returns a hash WITHOUT the screenshot property. This is on purpose! The screenshot should
14
14
  # not be included as part of the json.
15
15
  def to_hash
16
16
  {
@@ -25,26 +25,31 @@ module Applitools::Selenium
25
25
  Applitools::EyesLogger.debug "Retry timeout set to: #{retry_timeout}"
26
26
 
27
27
  start = Time.now
28
- res = if retry_timeout.zero?
29
- run(region, tag, rotation)
30
- elsif run_once_after_wait
31
- run(region, tag, rotation, retry_timeout)
32
- else
33
- run_with_intervals(region, tag, rotation, retry_timeout)
34
- end
28
+ res =
29
+ if retry_timeout.zero?
30
+ run(region, tag, rotation)
31
+ elsif run_once_after_wait
32
+ run(region, tag, rotation, retry_timeout)
33
+ else
34
+ run_with_intervals(region, tag, rotation, retry_timeout)
35
+ end
35
36
  elapsed_time = Time.now - start
36
37
 
37
38
  Applitools::EyesLogger.debug "match_window(): Completed in #{format('%.2f', elapsed_time)} seconds"
38
39
 
39
40
  @last_checked_window = @current_screenshot
40
- @last_screenshot_bounds = region.empty? ? Applitools::Base::Region.new(0, 0, last_checked_window.width,
41
- last_checked_window.height) : region
41
+ @last_screenshot_bounds =
42
+ if region.empty?
43
+ Applitools::Base::Region.new(0, 0, last_checked_window.width, last_checked_window.height)
44
+ else
45
+ region
46
+ end
42
47
  driver.clear_user_inputs
43
48
 
44
49
  res
45
50
  end
46
51
 
47
- def run(region, tag, rotation, wait_before_run=nil)
52
+ def run(region, tag, rotation, wait_before_run = nil)
48
53
  Applitools::EyesLogger.debug 'Trying matching once...'
49
54
 
50
55
  if wait_before_run
@@ -85,10 +90,12 @@ module Applitools::Selenium
85
90
  private
86
91
 
87
92
  def get_clipped_region(region, image)
88
- left, top = [region.left, 0].max, [region.top, 0].max
93
+ left = [region.left, 0].max
94
+ top = [region.top, 0].max
89
95
  max_width = image.width - left
90
96
  max_height = image.height - top
91
- width, height = [region.width, max_width].min, [region.height, max_height].min
97
+ width = [region.width, max_width].min
98
+ height = [region.height, max_height].min
92
99
  Applitools::Base::Region.new(left, top, width, height)
93
100
  end
94
101
 
@@ -130,12 +137,12 @@ module Applitools::Selenium
130
137
  trigger.control.intersect(last_screenshot_bounds)
131
138
  if trigger.control.empty?
132
139
  trigger_left -= - last_screenshot_bounds.left
133
- trigger_top = trigger_top - last_screenshot_bounds.top
140
+ trigger_top -= last_screenshot_bounds.top
134
141
  updated_trigger = Applitools::Base::MouseTrigger.new(trigger.mouse_action, trigger.control,
135
142
  Applitools::Base::Point.new(trigger_left, trigger_top))
136
143
  else
137
- trigger_left = trigger_left - trigger.control.left
138
- trigger_top = trigger_top - trigger.control.top
144
+ trigger_left -= trigger.control.left
145
+ trigger_top -= trigger.control.top
139
146
  control_left = trigger.control.left - last_screenshot_bounds.left
140
147
  control_top = trigger.control.top - last_screenshot_bounds.top
141
148
  updated_control = Applitools::Base::Region.new(control_left, control_top, trigger.control.width,
@@ -149,9 +156,9 @@ module Applitools::Selenium
149
156
  Applitools::EyesLogger.info "Trigger ignored: #{trigger} (out of bounds)"
150
157
  end
151
158
  elsif trigger.is_a?(Applitools::Base::TextTrigger)
152
- unless trigger.control.empty?
159
+ if !trigger.control.empty?
153
160
  trigger.control.intersect(last_screenshot_bounds)
154
- unless trigger.control.empty?
161
+ if !trigger.control.empty?
155
162
  control_left = trigger.control.left - last_screenshot_bounds.left
156
163
  control_top = trigger.control.top - last_screenshot_bounds.top
157
164
  updated_control = Applitools::Base::Region.new(control_left, control_top, trigger.control.width,
@@ -179,7 +186,7 @@ module Applitools::Selenium
179
186
  match_window_data_obj
180
187
  end
181
188
 
182
- def match(region, tag, rotation, ignore_mismatch=false)
189
+ def match(region, tag, rotation, ignore_mismatch = false)
183
190
  Applitools::EyesLogger.debug 'Match called...'
184
191
  data = prep_match_data(region, tag, rotation, ignore_mismatch)
185
192
  match_result = Applitools::Base::ServerConnector.match_window(session, data)
@@ -35,16 +35,16 @@ module Applitools::Selenium
35
35
  current_control = Applitools::Base::Region.new(0, 0, *location.values)
36
36
  driver.user_inputs << Applitools::Base::MouseTrigger.new(:move, current_control, location)
37
37
  element = element.web_element if element.is_a?(Applitools::Selenium::Element)
38
- mouse.move_to(element,right_by, down_by)
38
+ mouse.move_to(element, right_by, down_by)
39
39
  end
40
40
 
41
41
  def move_by(right_by, down_by)
42
42
  right = [0, right_by].max.round
43
43
  down = [0, down_by].max.round
44
- location = Applitools::Base::Point.new(right,down)
44
+ location = Applitools::Base::Point.new(right, down)
45
45
  current_control = Applitools::Base::Region.new(0, 0, right, down)
46
46
  driver.user_inputs << Applitools::Base::MouseTrigger.new(:move, current_control, location)
47
- mouse.move_by(right_by,down_by)
47
+ mouse.move_by(right_by, down_by)
48
48
  end
49
49
 
50
50
  private
@@ -1,37 +1,41 @@
1
1
  module Applitools::Selenium
2
2
  class ViewportSize
3
- JS_GET_VIEWPORT_HEIGHT = <<-EOF
4
- var height = undefined;
5
- if (window.innerHeight) {
6
- height = window.innerHeight;
7
- }
8
- else if (document.documentElement && document.documentElement.clientHeight) {
9
- height = document.documentElement.clientHeight;
10
- } else {
11
- var b = document.getElementsByTagName("body")[0];
12
- if (b.clientHeight) {
13
- height = b.clientHeight;
3
+ JS_GET_VIEWPORT_HEIGHT = (<<-JS).freeze
4
+ return (function() {
5
+ var height = undefined;
6
+ if (window.innerHeight) {
7
+ height = window.innerHeight;
14
8
  }
15
- }
16
-
17
- return height;
18
- EOF
19
-
20
- JS_GET_VIEWPORT_WIDTH = <<-EOF
21
- var width = undefined;
22
- if (window.innerWidth) {
23
- width = window.innerWidth
24
- } else if (document.documentElement && document.documentElement.clientWidth) {
25
- width = document.documentElement.clientWidth;
26
- } else {
27
- var b = document.getElementsByTagName("body")[0];
28
- if (b.clientWidth) {
29
- width = b.clientWidth;
9
+ else if (document.documentElement && document.documentElement.clientHeight) {
10
+ height = document.documentElement.clientHeight;
11
+ } else {
12
+ var b = document.getElementsByTagName("body")[0];
13
+ if (b.clientHeight) {
14
+ height = b.clientHeight;
15
+ }
30
16
  }
31
- }
32
17
 
33
- return width;
34
- EOF
18
+ return height;
19
+ }());
20
+ JS
21
+
22
+ JS_GET_VIEWPORT_WIDTH = (<<-JS).freeze
23
+ return (function() {
24
+ var width = undefined;
25
+ if (window.innerWidth) {
26
+ width = window.innerWidth
27
+ } else if (document.documentElement && document.documentElement.clientWidth) {
28
+ width = document.documentElement.clientWidth;
29
+ } else {
30
+ var b = document.getElementsByTagName("body")[0];
31
+ if (b.clientWidth) {
32
+ width = b.clientWidth;
33
+ }
34
+ }
35
+
36
+ return width;
37
+ }());
38
+ JS
35
39
 
36
40
  VERIFY_SLEEP_PERIOD = 1.freeze
37
41
  VERIFY_RETRIES = 3.freeze
@@ -54,7 +58,8 @@ module Applitools::Selenium
54
58
  end
55
59
 
56
60
  def extract_viewport_from_browser
57
- width, height = nil, nil
61
+ width = nil
62
+ height = nil
58
63
  begin
59
64
  width = extract_viewport_width
60
65
  height = extract_viewport_height
@@ -63,51 +68,52 @@ module Applitools::Selenium
63
68
  end
64
69
 
65
70
  if width.nil? || height.nil?
66
- Applitools::EyesLogger.info "Using window size as viewport size."
71
+ Applitools::EyesLogger.info 'Using window size as viewport size.'
67
72
 
68
73
  width, height = *browser_size.values
69
- width, height = width.ceil, height.ceil
74
+ width = width.ceil
75
+ height = height.ceil
70
76
 
71
77
  if @driver.landscape_orientation? && height > width
72
78
  width, height = height, width
73
79
  end
74
80
  end
75
81
 
76
- Applitools::Base::Dimension.new(width,height)
82
+ Applitools::Base::Dimension.new(width, height)
77
83
  end
78
84
 
79
85
  alias_method :viewport_size, :extract_viewport_from_browser
80
86
 
81
87
  def set
82
- if @dimension.is_a?(Hash) && @dimension.has_key?(:width) && @dimension.has_key?(:height)
88
+ if @dimension.is_a?(Hash) && @dimension.key?(:width) && @dimension.key?(:height)
83
89
  # If @dimension is hash of width/height, we convert it to a struct with width/height properties.
84
90
  @dimension = Struct.new(:width, :height).new(@dimension[:width], @dimension[:height])
85
91
  elsif !@dimension.respond_to?(:width) || !@dimension.respond_to?(:height)
86
- raise ArgumentError, "expected #{@dimension.inspect}:#{@dimension.class} to respond to #width and #height, or be "\
87
- ' a hash with these keys.'
92
+ raise ArgumentError, "expected #{@dimension.inspect}:#{@dimension.class} to respond to #width and #height, or "\
93
+ 'be a hash with these keys.'
88
94
  end
89
95
 
90
- set_browser_size(@dimension)
96
+ resize_browser(@dimension)
91
97
  verify_size(:browser_size)
92
98
 
93
- cur_viewport_size = extract_viewport_from_browser
99
+ current_viewport_size = extract_viewport_from_browser
94
100
 
95
- set_browser_size(Applitools::Base::Dimension.new((2 * browser_size.width) - cur_viewport_size.width,
96
- (2 * browser_size.height) - cur_viewport_size.height))
101
+ resize_browser(Applitools::Base::Dimension.new((2 * browser_size.width) - current_viewport_size.width,
102
+ (2 * browser_size.height) - current_viewport_size.height))
97
103
  verify_size(:viewport_size)
98
104
  end
99
105
 
100
- def verify_size(to_verify, sleep_time = VERIFY_SLEEP_PERIOD, retries = VERIFY_RETRIES)
101
- cur_size = nil
106
+ def verify_size(to_verify)
107
+ current_size = nil
102
108
 
103
- retries.times do
104
- sleep(sleep_time)
105
- cur_size = send(to_verify)
109
+ VERIFY_RETRIES.times do
110
+ sleep(VERIFY_SLEEP_PERIOD)
111
+ current_size = send(to_verify)
106
112
 
107
- return if cur_size.values == @dimension.values
113
+ return if current_size.values == @dimension.values
108
114
  end
109
115
 
110
- err_msg = "Failed setting #{to_verify} to #{@dimension.values} (current size: #{cur_size.values})"
116
+ err_msg = "Failed setting #{to_verify} to #{@dimension.values} (current size: #{current_size.values})"
111
117
 
112
118
  Applitools::EyesLogger.error(err_msg)
113
119
  raise Applitools::TestFailedError.new(err_msg)
@@ -117,7 +123,10 @@ module Applitools::Selenium
117
123
  @driver.manage.window.size
118
124
  end
119
125
 
120
- def set_browser_size(other)
126
+ def resize_browser(other)
127
+ # Before resizing the window, set its position to the upper left corner (otherwise, there might not be enough
128
+ # "space" below/next to it and the operation won't be successful).
129
+ @driver.manage.window.position = Selenium::WebDriver::Point.new(0, 0)
121
130
  @driver.manage.window.size = other
122
131
  end
123
132
 
@@ -1,150 +1,148 @@
1
- require 'oily_png'
2
-
3
- module Applitools::Utils
4
- module ImageDeltaCompressor
5
- extend self
6
-
7
- BLOCK_SIZE = 10.freeze
8
-
9
- # Compresses the target image based on the source image.
10
- #
11
- # +target+:: +ChunkyPNG::Canvas+ The image to compress based on the source image.
12
- # +target_encoded+:: +Array+ The uncompressed image as binary string.
13
- # +source+:: +ChunkyPNG::Canvas+ The source image used as a base for compressing the target image.
14
- # +block_size+:: +Integer+ The width/height of each block.
15
- #
16
- # Returns +String+ The binary result (either the compressed image, or the uncompressed image if the compression
17
- # is greater in length).
18
- def compress_by_raw_blocks(target, target_encoded, source, block_size = BLOCK_SIZE)
19
- # If we can't compress for any reason, return the target image as is.
20
- if source.nil? || (source.height != target.height) || (source.width != target.width)
21
- # Returning a COPY of the target binary string.
22
- return String.new(target_encoded)
23
- end
24
-
25
- # Preparing the variables we need.
26
- target_pixels = target.to_rgb_stream.unpack('C*')
27
- source_pixels = source.to_rgb_stream.unpack('C*')
28
- image_size = Dimension.new(target.width, target.height)
29
- block_columns_count = (target.width / block_size) + ((target.width % block_size) == 0 ? 0 : 1)
30
- block_rows_count = (target.height / block_size) + ((target.height % block_size) == 0 ? 0 : 1)
31
-
32
- # IMPORTANT: The "-Zlib::MAX_WBITS" tells ZLib to create raw deflate compression, without the
33
- # "Zlib headers" (this isn't documented in the Zlib page, I found this in some internet forum).
34
- compressor = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)
35
-
36
- compression_result = ''
37
-
38
- # Writing the data header.
39
- compression_result += PREAMBLE.encode('UTF-8')
40
- compression_result += [FORMAT_RAW_BLOCKS].pack('C')
41
- compression_result += [0].pack('S>') #Source id, Big Endian
42
- compression_result += [block_size].pack('S>') #Big Endian
43
-
44
- # We perform the compression for each channel.
45
- 3.times do |channel|
46
- block_number = 0
47
- block_rows_count.times do |block_row|
48
- block_columns_count.times do |block_column|
49
- actual_channel_index = 2 - channel # Since the image bytes are BGR and the server expects RGB...
50
- compare_result = compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, 3, block_size,
51
- block_column, block_row, actual_channel_index)
52
-
53
- unless compare_result.identical
54
- channel_bytes = compare_result.channel_bytes
55
- string_to_compress = [channel].pack('C')
56
- string_to_compress += [block_number].pack('L>')
57
- string_to_compress += channel_bytes.pack('C*')
58
-
59
- compression_result += compressor.deflate(string_to_compress)
60
-
61
- # If the compressed data so far is greater than the uncompressed representation of the target, just return
62
- # the target.
63
- if compression_result.length > target_encoded.length
64
- compressor.finish
65
- compressor.close
66
- # Returning a copy of the target bytes.
67
- return String.new(target_encoded)
68
- end
69
- end
70
-
71
- block_number += 1
72
- end
73
- end
74
- end
75
-
76
- # Compress and flush any remaining uncompressed data in the input buffer.
77
- compression_result += compressor.finish
78
- compressor.close
79
-
80
- # Returning the compressed result as a byte array.
81
- compression_result
82
- end
83
-
84
- private
85
-
86
- PREAMBLE = 'applitools'.freeze
87
- FORMAT_RAW_BLOCKS = 3.freeze
88
-
89
- Dimension = Struct.new(:width, :height)
90
- CompareAndCopyBlockChannelDataResult = Struct.new(:identical, :channel_bytes)
91
-
92
- # Computes the width and height of the image data contained in the block at the input column and row.
93
- # +image_size+:: +Dimension+ The image size in pixels.
94
- # +block_size+:: The block size for which we would like to compute the image data width and height.
95
- # +block_column+:: The block column index.
96
- # +block_row+:: The block row index.
97
- # ++
98
- # Returns the width and height of the image data contained in the block are returned as a +Dimension+.
99
- def get_actual_block_size(image_size, block_size, block_column, block_row)
100
- actual_width = [image_size.width - (block_column * block_size), block_size].min
101
- actual_height = [image_size.height - (block_row * block_size), block_size].min
102
- Dimension.new(actual_width, actual_height)
103
- end
104
-
105
- # Compares a block of pixels between the source and target and copies the target's block bytes to the result.
106
- # +source_pixels+:: +Array+ of bytes, representing the pixels of the source image.
107
- # +target_pixels+:: +Array+ of bytes, representing the pixels of the target image.
108
- # +image_size+:: +Dimension+ The size of the source/target image (remember they must be the same size).
109
- # +pixel_length+:: +Integer+ The number of bytes composing a pixel
110
- # +block_size+:: +Integer+ The width/height of the block (block is a square, theoretically).
111
- # +block_column+:: +Integer+ The block column index (when looking at the images as a grid of blocks).
112
- # +block_row+:: +Integer+ The block row index (when looking at the images as a grid of blocks).
113
- # +channel+:: +Integer+ The index of the channel we're comparing.
114
- # ++
115
- # Returns +CompareAndCopyBlockChannelDataResult+ object containing a flag saying whether the blocks are identical
116
- # and a copy of the target block's bytes.
117
- def compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, pixel_length, block_size,
118
- block_column, block_row, channel)
119
- identical = true
120
-
121
- actual_block_size = get_actual_block_size(image_size, block_size, block_column, block_row)
122
-
123
- # Getting the actual amount of data in the block we wish to copy.
124
- actual_block_height = actual_block_size.height
125
- actual_block_width = actual_block_size.width
126
-
127
- stride = image_size.width * pixel_length
128
-
129
- # Iterating the block's pixels and comparing the source and target.
130
- channel_bytes = []
131
- actual_block_height.times do |h|
132
- offset = (((block_size * block_row) + h) * stride) + (block_size * block_column * pixel_length) + channel
133
- actual_block_width.times do |w|
134
- source_byte = source_pixels[offset]
135
- target_byte = target_pixels[offset]
136
- if source_byte != target_byte
137
- identical = false
138
- end
139
- channel_bytes << target_byte
140
- offset += pixel_length
141
- end
142
- end
143
-
144
- # Returning the compare-and-copy result.
145
- CompareAndCopyBlockChannelDataResult.new(identical, channel_bytes)
146
- end
147
-
148
- include Applitools::MethodTracer
149
- end
150
- end
1
+ require 'oily_png'
2
+
3
+ module Applitools::Utils
4
+ module ImageDeltaCompressor
5
+ extend self
6
+
7
+ BLOCK_SIZE = 10.freeze
8
+
9
+ # Compresses the target image based on the source image.
10
+ #
11
+ # +target+:: +ChunkyPNG::Canvas+ The image to compress based on the source image.
12
+ # +target_encoded+:: +Array+ The uncompressed image as binary string.
13
+ # +source+:: +ChunkyPNG::Canvas+ The source image used as a base for compressing the target image.
14
+ # +block_size+:: +Integer+ The width/height of each block.
15
+ #
16
+ # Returns +String+ The binary result (either the compressed image, or the uncompressed image if the compression
17
+ # is greater in length).
18
+ def compress_by_raw_blocks(target, target_encoded, source, block_size = BLOCK_SIZE)
19
+ # If we can't compress for any reason, return the target image as is.
20
+ if source.nil? || (source.height != target.height) || (source.width != target.width)
21
+ # Returning a COPY of the target binary string.
22
+ return String.new(target_encoded)
23
+ end
24
+
25
+ # Preparing the variables we need.
26
+ target_pixels = target.to_rgb_stream.unpack('C*')
27
+ source_pixels = source.to_rgb_stream.unpack('C*')
28
+ image_size = Dimension.new(target.width, target.height)
29
+ block_columns_count = (target.width / block_size) + ((target.width % block_size) == 0 ? 0 : 1)
30
+ block_rows_count = (target.height / block_size) + ((target.height % block_size) == 0 ? 0 : 1)
31
+
32
+ # IMPORTANT: The "-Zlib::MAX_WBITS" tells ZLib to create raw deflate compression, without the
33
+ # "Zlib headers" (this isn't documented in the Zlib page, I found this in some internet forum).
34
+ compressor = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)
35
+
36
+ compression_result = ''
37
+
38
+ # Writing the data header.
39
+ compression_result += PREAMBLE.encode('UTF-8')
40
+ compression_result += [FORMAT_RAW_BLOCKS].pack('C')
41
+ compression_result += [0].pack('S>') # Source id, Big Endian
42
+ compression_result += [block_size].pack('S>') # Big Endian
43
+
44
+ # We perform the compression for each channel.
45
+ 3.times do |channel|
46
+ block_number = 0
47
+ block_rows_count.times do |block_row|
48
+ block_columns_count.times do |block_column|
49
+ actual_channel_index = 2 - channel # Since the image bytes are BGR and the server expects RGB...
50
+ compare_result = compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, 3,
51
+ block_size, block_column, block_row, actual_channel_index)
52
+
53
+ unless compare_result.identical
54
+ channel_bytes = compare_result.channel_bytes
55
+ string_to_compress = [channel].pack('C')
56
+ string_to_compress += [block_number].pack('L>')
57
+ string_to_compress += channel_bytes.pack('C*')
58
+
59
+ compression_result += compressor.deflate(string_to_compress)
60
+
61
+ # If the compressed data so far is greater than the uncompressed representation of the target, just return
62
+ # the target.
63
+ if compression_result.length > target_encoded.length
64
+ compressor.finish
65
+ compressor.close
66
+ # Returning a copy of the target bytes.
67
+ return String.new(target_encoded)
68
+ end
69
+ end
70
+
71
+ block_number += 1
72
+ end
73
+ end
74
+ end
75
+
76
+ # Compress and flush any remaining uncompressed data in the input buffer.
77
+ compression_result += compressor.finish
78
+ compressor.close
79
+
80
+ # Returning the compressed result as a byte array.
81
+ compression_result
82
+ end
83
+
84
+ private
85
+
86
+ PREAMBLE = 'applitools'.freeze
87
+ FORMAT_RAW_BLOCKS = 3.freeze
88
+
89
+ Dimension = Struct.new(:width, :height)
90
+ CompareAndCopyBlockChannelDataResult = Struct.new(:identical, :channel_bytes)
91
+
92
+ # Computes the width and height of the image data contained in the block at the input column and row.
93
+ # +image_size+:: +Dimension+ The image size in pixels.
94
+ # +block_size+:: The block size for which we would like to compute the image data width and height.
95
+ # +block_column+:: The block column index.
96
+ # +block_row+:: The block row index.
97
+ # ++
98
+ # Returns the width and height of the image data contained in the block are returned as a +Dimension+.
99
+ def get_actual_block_size(image_size, block_size, block_column, block_row)
100
+ actual_width = [image_size.width - (block_column * block_size), block_size].min
101
+ actual_height = [image_size.height - (block_row * block_size), block_size].min
102
+ Dimension.new(actual_width, actual_height)
103
+ end
104
+
105
+ # Compares a block of pixels between the source and target and copies the target's block bytes to the result.
106
+ # +source_pixels+:: +Array+ of bytes, representing the pixels of the source image.
107
+ # +target_pixels+:: +Array+ of bytes, representing the pixels of the target image.
108
+ # +image_size+:: +Dimension+ The size of the source/target image (remember they must be the same size).
109
+ # +pixel_length+:: +Integer+ The number of bytes composing a pixel
110
+ # +block_size+:: +Integer+ The width/height of the block (block is a square, theoretically).
111
+ # +block_column+:: +Integer+ The block column index (when looking at the images as a grid of blocks).
112
+ # +block_row+:: +Integer+ The block row index (when looking at the images as a grid of blocks).
113
+ # +channel+:: +Integer+ The index of the channel we're comparing.
114
+ # ++
115
+ # Returns +CompareAndCopyBlockChannelDataResult+ object containing a flag saying whether the blocks are identical
116
+ # and a copy of the target block's bytes.
117
+ def compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, pixel_length, block_size,
118
+ block_column, block_row, channel)
119
+ identical = true
120
+
121
+ actual_block_size = get_actual_block_size(image_size, block_size, block_column, block_row)
122
+
123
+ # Getting the actual amount of data in the block we wish to copy.
124
+ actual_block_height = actual_block_size.height
125
+ actual_block_width = actual_block_size.width
126
+
127
+ stride = image_size.width * pixel_length
128
+
129
+ # Iterating the block's pixels and comparing the source and target.
130
+ channel_bytes = []
131
+ actual_block_height.times do |h|
132
+ offset = (((block_size * block_row) + h) * stride) + (block_size * block_column * pixel_length) + channel
133
+ actual_block_width.times do |_w|
134
+ source_byte = source_pixels[offset]
135
+ target_byte = target_pixels[offset]
136
+ identical = false if source_byte != target_byte
137
+ channel_bytes << target_byte
138
+ offset += pixel_length
139
+ end
140
+ end
141
+
142
+ # Returning the compare-and-copy result.
143
+ CompareAndCopyBlockChannelDataResult.new(identical, channel_bytes)
144
+ end
145
+
146
+ include Applitools::MethodTracer
147
+ end
148
+ end