]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/commitdiff
Fewer bugs, more code, shape done
authorMelissa LeBlanc-Williams <melissa@adafruit.com>
Fri, 22 Sep 2023 21:25:07 +0000 (14:25 -0700)
committerMelissa LeBlanc-Williams <melissa@adafruit.com>
Fri, 22 Sep 2023 21:25:07 +0000 (14:25 -0700)
12 files changed:
displayio/__init__.py
displayio/_area.py
displayio/_bitmap.py
displayio/_colorconverter.py
displayio/_display.py
displayio/_displaycore.py
displayio/_group.py
displayio/_helpers.py
displayio/_i2cdisplay.py
displayio/_palette.py
displayio/_shape.py
displayio/_tilegrid.py

index 74facd99e0a288cbe35c5f53259bd4c80a6fe7f2..fad912245754a95d6d600a209f7882725a650986 100644 (file)
@@ -43,8 +43,9 @@ display_buses = []
 
 def _background():
     """Main thread function to loop through all displays and update them"""
-    for display in displays:
-        display.background()
+    while True:
+        for display in displays:
+            display.background()
 
 
 def release_displays() -> None:
index 5b96e9370bf4e6b72b276f0e6bae3c89f9de897d..f7272dd747bf25ae79ca94987ce278f0914b797a 100644 (file)
@@ -38,19 +38,22 @@ class Area:
     def __str__(self):
         return f"Area TL({self.x1},{self.y1}) BR({self.x2},{self.y2})"
 
-    def _copy_into(self, dst) -> None:
+    def copy_into(self, dst) -> None:
+        """Copy the area into another area."""
         dst.x1 = self.x1
         dst.y1 = self.y1
         dst.x2 = self.x2
         dst.y2 = self.y2
 
-    def _scale(self, scale: int) -> None:
+    def scale(self, scale: int) -> None:
+        """Scale the area by scale."""
         self.x1 *= scale
         self.y1 *= scale
         self.x2 *= scale
         self.y2 *= scale
 
-    def _shift(self, dx: int, dy: int) -> None:
+    def shift(self, dx: int, dy: int) -> None:
+        """Shift the area by dx and dy."""
         self.x1 += dx
         self.y1 += dy
         self.x2 += dx
@@ -70,21 +73,24 @@ class Area:
 
         return overlap.y1 < overlap.y2
 
-    def _empty(self):
+    def empty(self):
+        """Return True if the area is empty."""
         return (self.x1 == self.x2) or (self.y1 == self.y2)
 
-    def _canon(self):
+    def canon(self):
+        """Make sure the area is in canonical form."""
         if self.x1 > self.x2:
             self.x1, self.x2 = self.x2, self.x1
         if self.y1 > self.y2:
             self.y1, self.y2 = self.y2, self.y1
 
-    def _union(self, other, union):
-        if self._empty():
-            self._copy_into(union)
+    def union(self, other, union):
+        """Combine this area along with another into union"""
+        if self.empty():
+            self.copy_into(union)
             return
-        if other._empty():  # pylint: disable=protected-access
-            other._copy_into(union)  # pylint: disable=protected-access
+        if other.empty():
+            other.copy_into(union)
             return
 
         union.x1 = min(self.x1, other.x1)
index c61de47217a982550ccf4c6451aa9f822bf199a1..03dd75bf483c8f0e17e1910470156114fa591d9e 100644 (file)
@@ -21,6 +21,7 @@ from __future__ import annotations
 from typing import Union, Tuple
 from PIL import Image
 from ._structs import RectangleStruct
+from ._area import Area
 
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
@@ -76,6 +77,9 @@ class Bitmap:
 
         if x > self._image.width or y > self._image.height:
             raise ValueError(f"Index {index} is out of range")
+        return self._get_pixel(x, y)
+
+    def _get_pixel(self, x: int, y: int) -> int:
         return self._image.getpixel((x, y))
 
     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
@@ -129,8 +133,8 @@ class Bitmap:
         y2: int,
         skip_index: int,
     ) -> None:
-        # pylint: disable=unnecessary-pass, invalid-name
         """Inserts the source_bitmap region defined by rectangular boundaries"""
+        # pylint: disable=invalid-name
         if x2 is None:
             x2 = source_bitmap.width
         if y2 is None:
@@ -172,9 +176,28 @@ class Bitmap:
                     break
 
     def dirty(self, x1: int = 0, y1: int = 0, x2: int = -1, y2: int = -1) -> None:
-        # pylint: disable=unnecessary-pass, invalid-name
         """Inform displayio of bitmap updates done via the buffer protocol."""
-        pass
+        # pylint: disable=invalid-name
+        if x2 == -1:
+            x2 = self._bmp_width
+        if y2 == -1:
+            y2 = self._bmp_height
+        area = Area(x1, y1, x2, y2)
+        area.canon()
+        area.union(self._dirty_area, area)
+        bitmap_area = Area(0, 0, self._bmp_width, self._bmp_height)
+        area.compute_overlap(bitmap_area, self._dirty_area)
+
+    def _finish_refresh(self):
+        if self._read_only:
+            return
+        self._dirty_area.x1 = 0
+        self._dirty_area.x2 = 0
+
+    def _get_refresh_areas(self, areas: list[Area]) -> None:
+        if self._dirty_area.x1 == self._dirty_area.x2 or self._read_only:
+            return
+        areas.append(self._dirty_area)
 
     @property
     def width(self) -> int:
index db57b6a9c1756923656a66ae1d7dee9dfedbb727..f8d8deb69c8c67fc93e444289e1eddc4f579696e 100644 (file)
@@ -45,6 +45,7 @@ class ColorConverter:
         self._cached_colorspace = None
         self._cached_input_pixel = None
         self._cached_output_color = None
+        self._needs_refresh = False
 
     @staticmethod
     def _dither_noise_1(noise):
@@ -84,7 +85,7 @@ class ColorConverter:
         red8 = color_rgb888 >> 16
         grn8 = (color_rgb888 >> 8) & 0xFF
         blu8 = color_rgb888 & 0xFF
-        return (red8 * 19 + grn8 * 182 + blu8 + 54) / 255
+        return (red8 * 19 + grn8 * 182 + blu8 + 54) // 255
 
     @staticmethod
     def _compute_chroma(color_rgb888: int):
@@ -104,11 +105,11 @@ class ColorConverter:
             return 0
         hue = 0
         if max_color == red8:
-            hue = (((grn8 - blu8) * 40) / chroma) % 240
+            hue = (((grn8 - blu8) * 40) // chroma) % 240
         elif max_color == grn8:
-            hue = (((blu8 - red8) + (2 * chroma)) * 40) / chroma
+            hue = (((blu8 - red8) + (2 * chroma)) * 40) // chroma
         elif max_color == blu8:
-            hue = (((red8 - grn8) + (4 * chroma)) * 40) / chroma
+            hue = (((red8 - grn8) + (4 * chroma)) * 40) // chroma
         if hue < 0:
             hue += 240
 
index 8e33803a7293df2df6e27eac7650ac30c0638080..cdf9f7860e54310306422d09d3b6b9a65779feab 100644 (file)
@@ -191,8 +191,9 @@ class Display:
             allocate_display,
         )
 
-        allocate_display(cls)
-        return super().__new__(cls)
+        display_instance = super().__new__(cls)
+        allocate_display(display_instance)
+        return display_instance
 
     def _initialize(self, init_sequence):
         i = 0
@@ -290,12 +291,12 @@ class Display:
         return True
 
     def _refresh_display(self):
-        # pylint: disable=protected-access
         if not self._core.start_refresh():
             return False
 
         # TODO: Likely move this to _refresh_area()
         # Go through groups and and add each to buffer
+        """
         if self._core.current_group is not None:
             buffer = Image.new("RGBA", (self._core.width, self._core.height))
             # Recursively have everything draw to the image
@@ -304,7 +305,7 @@ class Display:
             )  # pylint: disable=protected-access
             # save image to buffer (or probably refresh buffer so we can compare)
             self._buffer.paste(buffer)
-
+        """
         areas_to_refresh = self._get_refresh_areas()
 
         for area in areas_to_refresh:
@@ -317,9 +318,12 @@ class Display:
     def _get_refresh_areas(self) -> list[Area]:
         """Get a list of areas to be refreshed"""
         areas = []
-        if self._core.current_group is not None:
-            # Eventually calculate dirty rectangles here
-            areas.append(Area(0, 0, self._core.width, self._core.height))
+        if self._core.full_refresh:
+            areas.append(self._core.area)
+        elif self._core.current_group is not None:
+            self._core.current_group._get_refresh_areas(  # pylint: disable=protected-access
+                areas
+            )
         return areas
 
     def background(self):
index 3126b8732da7df9a27a42ed36371513ad5ed7266..d2f298a07a992fd8f232823e39285da901c9c4b9 100644 (file)
@@ -258,21 +258,6 @@ class _DisplayCore:
             )
         return False
 
-    """
-    def _clip(self, rectangle):
-        if self._core.rotation in (90, 270):
-            width, height = self._core.height, self._core.width
-        else:
-            width, height = self._core.width, self._core.height
-
-        rectangle.x1 = max(rectangle.x1, 0)
-        rectangle.y1 = max(rectangle.y1, 0)
-        rectangle.x2 = min(rectangle.x2, width)
-        rectangle.y2 = min(rectangle.y2, height)
-
-        return rectangle
-    """
-
     def clip_area(self, area: Area, clipped: Area) -> bool:
         """Shrink the area to the region shared by the two areas"""
 
@@ -308,11 +293,11 @@ class _DisplayCore:
         if self.colorspace.depth < 8:
             pixels_per_byte = 8 // self.colorspace.depth
             if self.colorspace.pixels_in_byte_share_row:
-                region_x1 /= pixels_per_byte * self.colorspace.bytes_per_cell
-                region_x2 /= pixels_per_byte * self.colorspace.bytes_per_cell
+                region_x1 //= pixels_per_byte * self.colorspace.bytes_per_cell
+                region_x2 //= pixels_per_byte * self.colorspace.bytes_per_cell
             else:
-                region_y1 /= pixels_per_byte * self.colorspace.bytes_per_cell
-                region_y2 /= pixels_per_byte * self.colorspace.bytes_per_cell
+                region_y1 //= pixels_per_byte * self.colorspace.bytes_per_cell
+                region_y2 //= pixels_per_byte * self.colorspace.bytes_per_cell
 
         region_x2 -= 1
         region_y2 -= 1
@@ -330,12 +315,6 @@ class _DisplayCore:
             )
         else:
             data_type = DISPLAY_COMMAND
-            """
-            self._core.send(
-                DISPLAY_COMMAND, CHIP_SELECT_TOGGLE_EVERY_BYTE, bytes([command]) + data
-            )
-            self._core.send(DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data)
-            """
 
         if self.ram_width < 0x100:  # Single Byte Bounds
             data = struct.pack(">BB", region_x1, region_x2)
@@ -426,6 +405,7 @@ class _DisplayCore:
         """
         Send the data to the current bus
         """
+        print(data_type, chip_select, data)
         self._send(data_type, chip_select, data)
 
     def begin_transaction(self) -> None:
index a23ae4e4600973ffa979aacfbcd26aa5a55f92d9..903d7218380bfb84f145907081df9adc978e3cdf 100644 (file)
@@ -52,6 +52,7 @@ class Group:
         self._layers = []
         self._supported_types = (TileGrid, Group)
         self._in_group = False
+        self._item_removed = False
         self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
         self._set_scale(scale)  # Set the scale via the setter
 
@@ -166,6 +167,12 @@ class Group:
             if isinstance(layer, (Group, TileGrid)):
                 layer._finish_refresh()  # pylint: disable=protected-access
 
+    def _get_refresh_areas(self, areas: list[Area]) -> None:
+        for layer in self._layers:
+            if isinstance(layer, (Group, TileGrid)):
+                if not layer.hidden:
+                    layer._get_refresh_areas(areas)  # pylint: disable=protected-access
+
     @property
     def hidden(self) -> bool:
         """True when the Group and all of it's layers are not visible. When False, the
index cbff9f40a72c91432e1742cd85a3d79fe55b990e..39e1c11c3a7c1bcf1d9c881aa6f82eac73a35df6 100644 (file)
@@ -20,10 +20,12 @@ displayio for Blinka
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
+
 def clamp(value, min_value, max_value):
     """Clamp a value between a minimum and maximum value"""
     return max(min(max_value, value), min_value)
 
+
 def bswap16(value):
     """Swap the bytes in a 16 bit value"""
-    return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
\ No newline at end of file
+    return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8
index b5de381edced2f74620a106204a2ffa6352bdfd8..67022f5c213048f4704d398d7a72f3429d270cf4 100644 (file)
@@ -58,8 +58,9 @@ class I2CDisplay:
             allocate_display_bus,
         )
 
-        allocate_display_bus(cls)
-        return super().__new__(cls)
+        display_bus_instance = super().__new__(cls)
+        allocate_display_bus(display_bus_instance)
+        return display_bus_instance
 
     def _release(self):
         self.reset()
index 2424db1b9839ed48f7bc1b249c2c6c79bb9fc509..af72dcbf4c1b216fa10eeec6c3207b77c8aa5d13 100644 (file)
@@ -139,6 +139,9 @@ class Palette:
         """Returns True if the palette index is transparent. Returns False if opaque."""
         return self._colors[palette_index].transparent
 
+    def _finish_refresh(self):
+        pass
+
     @property
     def dither(self) -> bool:
         """When true the palette dithers the output by adding
index 946919d41fcf804fa7336d5a35fadf820147a445..81d15a0663cb2002d4f8344670de9f308b022ac4 100644 (file)
@@ -18,7 +18,10 @@ displayio for Blinka
 
 """
 
+import struct
 from ._bitmap import Bitmap
+from ._area import Area
+from ._helpers import clamp
 
 __version__ = "0.0.0+auto.0"
 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
@@ -33,14 +36,95 @@ class Shape(Bitmap):
     def __init__(
         self, width: int, height: int, *, mirror_x: bool = False, mirror_y: bool = False
     ):
-        # pylint: disable=unused-argument
         """Create a Shape object with the given fixed size. Each pixel is one bit and is
         stored by the column boundaries of the shape on each row. Each row’s boundary
         defaults to the full row.
         """
+        self._mirror_x = mirror_x
+        self._mirror_y = mirror_y
+        self._width = width
+        self._height = height
+        if self._mirror_x:
+            width //= 2
+            width += self._width % 2
+        self._half_width = width
+        if self._mirror_y:
+            height //= 2
+            height += self._height % 2
+        self._half_height = height
+        self._data = bytearray(height * struct.calcsize("HH"))
+        for i in range(height):
+            self._data[2 * i] = 0
+            self._data[2 * i + 1] = width
+
+        self._dirty_area = Area(0, 0, width, height)
         super().__init__(width, height, 2)
 
     def set_boundary(self, y: int, start_x: int, end_x: int) -> None:
-        # pylint: disable=unnecessary-pass
         """Loads pre-packed data into the given row."""
-        pass
+        max_y = self._height - 1
+        if self._mirror_y:
+            max_y = self._half_height - 1
+        y = clamp(y, 0, max_y)
+        max_x = self._width - 1
+        if self._mirror_x:
+            max_x = self._half_width - 1
+        start_x = clamp(start_x, 0, max_x)
+        end_x = clamp(end_x, 0, max_x)
+
+        # find x-boundaries for updating based on current data and start_x, end_x, and mirror_x
+        lower_x = min(start_x, self._data[2 * y])
+
+        if self._mirror_x:
+            upper_x = (
+                self._width - lower_x + 1
+            )  # dirty rectangles are treated with max value exclusive
+        else:
+            upper_x = max(
+                end_x, self._data[2 * y + 1]
+            )  # dirty rectangles are treated with max value exclusive
+
+        # find y-boundaries based on y and mirror_y
+        lower_y = y
+
+        if self._mirror_y:
+            upper_y = (
+                self._height - lower_y + 1
+            )  # dirty rectangles are treated with max value exclusive
+        else:
+            upper_y = y + 1  # dirty rectangles are treated with max value exclusive
+
+        self._data[2 * y] = start_x  # update the data array with the new boundaries
+        self._data[2 * y + 1] = end_x
+
+        if self._dirty_area.x1 == self._dirty_area.x2:  # dirty region is empty
+            self._dirty_area.x1 = lower_x
+            self._dirty_area.x2 = upper_x
+            self._dirty_area.y1 = lower_y
+            self._dirty_area.y2 = upper_y
+        else:
+            self._dirty_area.x1 = min(lower_x, self._dirty_area.x1)
+            self._dirty_area.x2 = max(upper_x, self._dirty_area.x2)
+            self._dirty_area.y1 = min(lower_y, self._dirty_area.y1)
+            self._dirty_area.y2 = max(upper_y, self._dirty_area.y2)
+
+    def _get_pixel(self, x: int, y: int) -> int:
+        if x >= self._width or x < 0 or y >= self._height or y < 0:
+            return 0
+        if self._mirror_x and x >= self._half_width:
+            x = self._width - x - 1
+        if self._mirror_y and y >= self._half_height:
+            y = self._height - y - 1
+        start_x = self._data[2 * y]
+        end_x = self._data[2 * y + 1]
+        if x < start_x or x >= end_x:
+            return 0
+        return 1
+
+    def _finish_refresh(self):
+        self._dirty_area.x1 = 0
+        self._dirty_area.x2 = 0
+
+    def _get_refresh_areas(self, areas: list[Area]) -> None:
+        if self._dirty_area.x1 != self._dirty_area.x2:
+            areas.append(self._dirty_area)
index 0cfc6d3d304bbe47dfb724f460406cf098e12535..72e57c006f111b5c97f486f8e7727a905583cfa7 100644 (file)
@@ -73,6 +73,8 @@ class TileGrid:
         if isinstance(self._pixel_shader, ColorConverter):
             self._pixel_shader._rgba = True  # pylint: disable=protected-access
         self._hidden_tilegrid = False
+        self._hidden_by_parent = False
+        self._rendered_hidden = False
         self._x = x
         self._y = y
         self._width_in_tiles = width
@@ -98,16 +100,21 @@ class TileGrid:
             raise ValueError("Default Tile is out of range")
         self._pixel_width = width * tile_width
         self._pixel_height = height * tile_height
-        self._tiles = (self._width_in_tiles * self._height_in_tiles) * [default_tile]
+        self._tiles = bytearray(
+            (self._width_in_tiles * self._height_in_tiles) * [default_tile]
+        )
         self._in_group = False
         self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
         self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
+        self._dirty_area = Area(0, 0, 0, 0)
+        self._previous_area = Area(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)
         self._moved = False
+        self._full_change = True
+        self._partial_change = True
         self._bitmap_width_in_tiles = bitmap_width // tile_width
         self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
             bitmap_height // tile_height
         )
-        self.inline_tiles = False  # We have plenty of memory
 
     def _update_transform(self, absolute_transform):
         """Update the parent transform and child transforms"""
@@ -121,6 +128,7 @@ class TileGrid:
             width = self._pixel_height
         else:
             width = self._pixel_width
+
         if self._absolute_transform.transpose_xy:
             self._current_area.y1 = (
                 self._absolute_transform.y + self._absolute_transform.dy * self._x
@@ -153,6 +161,7 @@ class TileGrid:
             height = self._pixel_width
         else:
             height = self._pixel_height
+
         if self._absolute_transform.transpose_xy:
             self._current_area.x1 = (
                 self._absolute_transform.x + self._absolute_transform.dx * self._y
@@ -210,7 +219,7 @@ class TileGrid:
         # If no tiles are present we have no impact
         tiles = self._tiles
 
-        if self._hidden_tilegrid:
+        if self._hidden_tilegrid or self._hidden_by_parent:
             return False
 
         overlap = Area()
@@ -230,10 +239,10 @@ class TileGrid:
 
         start = 0
         if (self._absolute_transform.dx < 0) != flip_x:
-            start += (area.width() - 1) * x_stride
+            start += (area.x2 - area.x1 - 1) * x_stride
             x_stride *= -1
         if (self._absolute_transform.dy < 0) != flip_y:
-            start += (area.height() - 1) * y_stride
+            start += (area.y2 - area.y1 - 1) * y_stride
             y_stride *= -1
 
         full_coverage = area == overlap
@@ -298,7 +307,11 @@ class TileGrid:
                     input_pixel.tile // self._bitmap_width_in_tiles
                 ) * self._tile_height + local_y % self._tile_height
 
-                input_pixel.pixel = self.bitmap[input_pixel.tile_x, input_pixel.tile_y]
+                input_pixel.pixel = (
+                    self._bitmap._get_pixel(  # pylint: disable=protected-access
+                        input_pixel.tile_x, input_pixel.tile_y
+                    )
+                )
                 output_pixel.opaque = True
 
                 if self._pixel_shader is None:
@@ -352,7 +365,108 @@ class TileGrid:
         return full_coverage
 
     def _finish_refresh(self):
-        pass
+        first_draw = self._previous_area.x1 == self._previous_area.x2
+        hidden = self._hidden_tilegrid or self._hidden_by_parent
+        if not first_draw and hidden:
+            self._previous_area.x2 = self._previous_area.x1
+        elif self._moved or first_draw:
+            self._current_area.copy_into(self._previous_area)
+
+        self._moved = False
+        self._full_change = False
+        self._partial_change = False
+        if isinstance(self._pixel_shader, (Palette, ColorConverter)):
+            self._pixel_shader._finish_refresh()  # pylint: disable=protected-access
+        if isinstance(self._bitmap, (Bitmap, Shape)):
+            self._bitmap._finish_refresh()  # pylint: disable=protected-access
+
+    def _get_refresh_areas(self, areas: list[Area]) -> None:
+        # pylint: disable=invalid-name, too-many-branches, too-many-statements
+        first_draw = self._previous_area.x1 == self._previous_area.x2
+        hidden = self._hidden_tilegrid or self._hidden_by_parent
+
+        # Check hidden first because it trumps all other changes
+        if hidden:
+            self._rendered_hidden = True
+            if not first_draw:
+                areas.append(self._previous_area)
+            return
+        if self._moved and not first_draw:
+            self._previous_area.union(self._current_area, self._dirty_area)
+            if self._dirty_area.size() < 2 * self._pixel_width * self._pixel_height:
+                areas.append(self._dirty_area)
+                return
+            areas.append(self._current_area)
+            areas.append(self._previous_area)
+            return
+
+        tail = areas[-1]
+        # If we have an in-memory bitmap, then check it for modifications
+        if isinstance(self._bitmap, Bitmap):
+            self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
+            if tail != areas[-1]:
+                # Special case a TileGrid that shows a full bitmap and use its
+                # dirty area. Copy it to ours so we can transform it.
+                if self._tiles_in_bitmap == 1:
+                    areas[-1].copy_into(self._dirty_area)
+                    self._partial_change = True
+                else:
+                    self._full_change = True
+        elif isinstance(self._bitmap, Shape):
+            self._bitmap._get_refresh_areas(areas)  # pylint: disable=protected-access
+            if areas[-1] != tail:
+                areas[-1].copy_into(self._dirty_area)
+                self._partial_change = True
+
+        self._full_change = self._full_change or (
+            isinstance(self._pixel_shader, (Palette, ColorConverter))
+            and self._pixel_shader._needs_refresh  # pylint: disable=protected-access
+        )
+        if self._full_change or first_draw:
+            areas.append(self._current_area)
+            return
+
+        if self._partial_change:
+            x = self._x
+            y = self._y
+            if self._absolute_transform.transpose_xy:
+                x, y = y, x
+            x1 = self._dirty_area.x1
+            x2 = self._dirty_area.x2
+            if self._flip_x:
+                x1 = self._pixel_width - x1
+                x2 = self._pixel_width - x2
+            y1 = self._dirty_area.y1
+            y2 = self._dirty_area.y2
+            if self._flip_y:
+                y1 = self._pixel_height - y1
+                y2 = self._pixel_height - y2
+            if self._transpose_xy != self._absolute_transform.transpose_xy:
+                x1, y1 = y1, x1
+                x2, y2 = y2, x2
+            self._dirty_area.x1 = (
+                self._absolute_transform.x + self._absolute_transform.dx * (x + x1)
+            )
+            self._dirty_area.y1 = (
+                self._absolute_transform.y + self._absolute_transform.dy * (y + y1)
+            )
+            self._dirty_area.x2 = (
+                self._absolute_transform.x + self._absolute_transform.dx * (x + x2)
+            )
+            self._dirty_area.y2 = (
+                self._absolute_transform.y + self._absolute_transform.dy * (y + y2)
+            )
+            if self._dirty_area.y2 < self._dirty_area.y1:
+                self._dirty_area.y1, self._dirty_area.y2 = (
+                    self._dirty_area.y2,
+                    self._dirty_area.y1,
+                )
+            if self._dirty_area.x2 < self._dirty_area.x1:
+                self._dirty_area.x1, self._dirty_area.x2 = (
+                    self._dirty_area.x2,
+                    self._dirty_area.x1,
+                )
+            areas.append(self._dirty_area)
 
     @property
     def hidden(self) -> bool: