eyes_selenium 2.16.0 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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