Class: Applitools::Utils::ImageDeltaCompressor

Inherits:
Object
  • Object
show all
Defined in:
lib/eyes_selenium_ruby/utils/image_delta_compressor.rb

Defined Under Namespace

Classes: CompareAndCopyBlockChannelDataResult, Dimension

Class Method Summary collapse

Class Method Details

.compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, pixel_length, block_size, block_column, block_row, channel) ⇒ Object

Compares a block of pixels between the source and target and copies the target’s block bytes to the result.

source_pixels

Array of bytes, representing the pixels of the source image.

target_pixels

Array of bytes, representing the pixels of the target image.

image_size

Dimension The size of the source/target image (remember they must be the same size).

pixel_length

Integer The number of bytes composing a pixel

block_size

Integer The width/height of the block (block is a square, theoretically).

block_column

Integer The block column index (when looking at the images as a grid of blocks).

block_row

Integer The block row index (when looking at the images as a grid of blocks).

channel

Integer The index of the channel we’re comparing.

++ Returns CompareAndCopyBlockChannelDataResult object containing a flag saying whether the blocks are identical and a copy of the target block’s bytes.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/eyes_selenium_ruby/utils/image_delta_compressor.rb', line 118

def self.compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, pixel_length, block_size,
    block_column, block_row, channel)
  identical = true

  actual_block_size = get_actual_block_size(image_size, block_size, block_column, block_row)

  # Getting the actual amount of data in the block we wish to copy
  actual_block_height = actual_block_size.height
  actual_block_width = actual_block_size.width

  stride = image_size.width * pixel_length

  # Iterating the block's pixels and comparing the source and target
  channel_bytes = []
  actual_block_height.times do |h|
    offset = (((block_size * block_row) + h) * stride) + (block_size * block_column * pixel_length) + channel
    actual_block_width.times do |w|
      source_byte = source_pixels[offset]
      target_byte = target_pixels[offset]
      if source_byte != target_byte
        identical = false
      end
      channel_bytes << target_byte
      offset += pixel_length
    end
  end
  # Returning the compare-and-copy result
  CompareAndCopyBlockChannelDataResult.new(identical, channel_bytes)
end

.compress_by_raw_blocks(target, target_encoded, source, block_size = 10) ⇒ Object

Compresses the target image based on the source image.

target

ChunkyPNG::Canvas The image to compress based on the source image.

target_encoded

Array The uncompressed image as binary string.

source

ChunkyPNG::Canvas The source image used as a base for compressing the target image.

block_size

Integer The width/height of each block.

++ Returns String The binary result (either the compressed image, or the uncompressed image if the compression is greater in length)



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/eyes_selenium_ruby/utils/image_delta_compressor.rb', line 19

def self.compress_by_raw_blocks(target, target_encoded, source, block_size = 10)
  # If we can't compress for any reason, return the target image as is.
  if source.nil? || (source.height != target.height) || (source.width != target.width)
    # Returning a COPY of the target binary string
    return String.new(target_encoded) 
  end

  # Preparing the variables we need
  target_pixels = target.to_rgb_stream.unpack('C*')
  source_pixels = source.to_rgb_stream.unpack('C*')
  image_size = Dimension.new(target.width, target.height)
  block_columns_count = (target.width / block_size) + ((target.width % block_size) == 0 ? 0 : 1)
  block_rows_count = (target.height / block_size) + ((target.height % block_size) == 0 ? 0 : 1)

  # IMPORTANT: The "-Zlib::MAX_WBITS" tells ZLib to create raw deflate compression, without the
  # "Zlib headers" (this isn't documented in the Zlib page, I found this in some internet forum).
  compressor = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)

  compression_result = ''

  # Writing the data header
  compression_result += @@PREAMBLE.encode("UTF-8")
  compression_result += [@@FORMAT_RAW_BLOCKS].pack("C")
  compression_result += [0].pack("S>") #Source id, Big Endian
  compression_result += [block_size].pack("S>") #Big Endian


  # We perform the compression for each channel
  3.times do |channel|
    block_number = 0
    block_rows_count.times do |block_row|
      block_columns_count.times do |block_column|
        actual_channel_index = 2 - channel # Since the image bytes are BGR and the server expects RGB...
        compare_result = compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, 3, block_size,
                                                             block_column, block_row, actual_channel_index)

        if !compare_result.identical
          channel_bytes = compare_result.channel_bytes
          string_to_compress = [channel].pack('C')
          string_to_compress += [block_number].pack('L>')
          string_to_compress += channel_bytes.pack('C*')
          compression_result += compressor.deflate(string_to_compress)

          # If the compressed data so far is greater than the uncompressed
          # representation of the target, just return the target.
          if compression_result.length > target_encoded.length
            compressor.close
            # Returning a COPY of the target bytes
            return String.new(target_encoded)
          end
        end
        block_number += 1
      end
    end
  end
  # Compress and flush any remaining uncompressed data in the input buffer.
  compression_result += compressor.finish
  compressor.close
  # Returning the compressed result as a byte array
  return compression_result
end

.get_actual_block_size(image_size, block_size, block_column, block_row) ⇒ Object

Computes the width and height of the image data contained in the block at the input column and row.

image_size

Dimension The image size in pixels.

block_size

The block size for which we would like to compute the image data width and height.

block_column

The block column index.

block_row

The block row index.

++ Returns the width and height of the image data contained in the block are returned as a Dimension.



100
101
102
103
104
# File 'lib/eyes_selenium_ruby/utils/image_delta_compressor.rb', line 100

def self.get_actual_block_size(image_size, block_size, block_column, block_row)
  actual_width = [image_size.width - (block_column * block_size), block_size].min
  actual_height = [image_size.height - (block_row * block_size), block_size].min
  Dimension.new(actual_width, actual_height)
end