"""Classes for defining how to crop screenshots in pixel-related tests."""
import abc
from telemetry.util import image_util
from gpu_tests import common_typing as ct
class BaseCropAction(abc.ABC):
@abc.abstractmethod
def CropScreenshot(self, screenshot: ct.Screenshot, dpr: float,
device_type: str, os_name: str) -> ct.Screenshot:
"""Return a cropped copy of |screenshot|.
The exact behavior is dependent on the concrete class.
"""
class NoOpCropAction(BaseCropAction):
def CropScreenshot(self, screenshot: ct.Screenshot, dpr: float,
device_type: str, os_name: str) -> ct.Screenshot:
del dpr, device_type, os_name
return screenshot
class FixedRectCropAction(BaseCropAction):
"""Crops screenshots to the given rectangle.
The rectangle is first scaled based on the device pixel ratio.
"""
SCROLLBAR_WIDTH = 12
def __init__(self, x1: int, y1: int, x2: int | None, y2: int | None):
"""
Args:
x1: An int specifying the x coordinate of the top left corner of the crop
rectangle
y1: An int specifying the y coordinate of the top left corner of the crop
rectangle
x2: An int specifying the x coordinate of the bottom right corner of the
crop rectangle. Can be None to explicitly specify the right side of
the image, although clamping will be performed regardless. Can be
negative to specify an offset relative to the right edge.
y2: An int specifying the y coordinate of the bottom right corner of the
crop rectangle. Can be None to explicitly specify the bottom of the
image, although clamping will be performed regardless. Can be
negative to specify an offset relative to the bottom.
"""
assert x1 >= 0
assert y1 >= 0
assert x2 is None or x2 > x1 or x2 < 0
assert y2 is None or y2 > y1 or y2 < 0
self._x1 = x1
self._y1 = y1
self._x2 = x2
self._y2 = y2
def CropScreenshot(self, screenshot: ct.Screenshot, dpr: float,
device_type: str, os_name: str) -> ct.Screenshot:
del device_type, os_name
start_x = int(self._x1 * dpr)
start_y = int(self._y1 * dpr)
image_width = image_util.Width(screenshot)
image_height = image_util.Height(screenshot)
max_x = image_width - FixedRectCropAction.SCROLLBAR_WIDTH
max_y = image_height
if self._x2 is None:
end_x = max_x
elif self._x2 < 0:
tentative_x = max(start_x + 1, int(self._x2 * dpr) + image_width)
end_x = min(tentative_x, max_x)
else:
end_x = min(int(self._x2 * dpr), max_x)
if self._y2 is None:
end_y = max_y
elif self._y2 < 0:
tentative_y = max(start_y + 1, int(self._y2 * dpr) + image_height)
end_y = min(tentative_y, max_y)
else:
end_y = min(int(self._y2 * dpr), max_y)
crop_width = end_x - start_x
crop_height = end_y - start_y
return image_util.Crop(screenshot, start_x, start_y, crop_width,
crop_height)
class NonWhiteContentCropAction(BaseCropAction):
"""Crops screenshots to remove all white (background) content."""
OFF_WHITE_TOP_ROW_DEVICES = {
'SM-A137F',
'SM-A236B',
'Brya',
'Corsola',
}
def __init__(self, initial_crop: BaseCropAction | None = None):
"""
Args:
initial_crop: An initial crop to perform before removing the background.
Intended to reduce the amount of work done finding the non-white
content if the content of interest is known to be small relative to
the entire screenshot.
"""
self._initial_crop = initial_crop
def CropScreenshot(self, screenshot: ct.Screenshot, dpr: float,
device_type: str, os_name: str) -> ct.Screenshot:
if os_name == 'mac':
screenshot = image_util.Crop(screenshot, 0, 0,
image_util.Width(screenshot),
image_util.Height(screenshot) - 20)
if device_type in NonWhiteContentCropAction.OFF_WHITE_TOP_ROW_DEVICES:
screenshot = image_util.Crop(screenshot, 0, 1,
image_util.Width(screenshot),
image_util.Height(screenshot) - 1)
if self._initial_crop:
screenshot = self._initial_crop.CropScreenshot(screenshot, dpr,
device_type, os_name)
x1, y1, x2, y2 = _GetNonWhiteCropBoundaries(screenshot)
return image_util.Crop(screenshot, x1, y1, x2 - x1, y2 - y1)
def _GetNonWhiteCropBoundaries(
screenshot: ct.Screenshot) -> tuple[int, int, int, int]:
"""Returns the boundaries to crop the screenshot to.
Specifically, we look for the boundaries where the white background
transitions into the (non-white) content we care about.
Returns:
A 4-tuple (x1, y1, x2, y2) denoting the top left and bottom right
coordinates to crop to.
"""
img_height = image_util.Height(screenshot)
img_width = image_util.Width(screenshot)
pixel_data = image_util.Pixels(screenshot)
channels = image_util.Channels(screenshot)
def RowIsWhite(row, start=None, end=None):
row_offset = row * img_width * channels
start = start or 0
end = end or img_width
for col in range(start, end):
col_offset = col * channels
pixel_index = row_offset + col_offset
r = pixel_data[pixel_index]
g = pixel_data[pixel_index + 1]
b = pixel_data[pixel_index + 2]
if r != 255 or g != 255 or b != 255:
return False
return True
def ColumnIsWhite(column, start=None, end=None):
column_offset = column * channels
start = start or 0
end = end or img_height
for row in range(start, end):
row_offset = row * img_width * channels
pixel_index = row_offset + column_offset
r = pixel_data[pixel_index]
g = pixel_data[pixel_index + 1]
b = pixel_data[pixel_index + 2]
if r != 255 or g != 255 or b != 255:
return False
return True
x1 = y1 = 0
x2 = img_width
y2 = img_height
for column in range(img_width):
if not ColumnIsWhite(column):
x1 = column
break
else:
raise RuntimeError(
'Attempted to crop to non-white content in an all white image')
for row in range(img_height):
if not RowIsWhite(row, start=x1):
y1 = row
break
for column in range(img_width - 1, x1 - 1, -1):
if not ColumnIsWhite(column, start=y1):
x2 = column + 1
break
for row in range(img_height - 1, y1 - 1, -1):
if not RowIsWhite(row, start=x1, end=x2):
y2 = row + 1
break
return x1, y1, x2, y2