from __future__ import annotations import os from PIL import Image VALID_ORIENTATIONS = {0, 90, 180, 270} DEFAULT_ROW_BIT_ORDER = (0, 1, 2, 3, 4) def apply_orientation(image: Image.Image, orientation: int) -> Image.Image: if orientation not in VALID_ORIENTATIONS: raise ValueError("orientation must be one of 0, 90, 180, 270") if orientation == 0: return image.copy() if orientation == 90: return image.transpose(Image.Transpose.ROTATE_270) if orientation == 180: return image.transpose(Image.Transpose.ROTATE_180) return image.transpose(Image.Transpose.ROTATE_90) def get_row_bit_order() -> tuple[int, int, int, int, int]: raw = os.environ.get("MATRIX_ROW_BIT_ORDER", "").strip() if not raw: return DEFAULT_ROW_BIT_ORDER try: values = tuple(int(part.strip()) for part in raw.split(",")) except ValueError as exc: raise ValueError("MATRIX_ROW_BIT_ORDER must be five comma-separated integers") from exc if len(values) != 5 or sorted(values) != [0, 1, 2, 3, 4]: raise ValueError("MATRIX_ROW_BIT_ORDER must be a permutation of 0,1,2,3,4") return values def apply_row_bit_order(image: Image.Image, bit_order: tuple[int, int, int, int, int]) -> Image.Image: if bit_order == DEFAULT_ROW_BIT_ORDER: return image.copy() width, height = image.size if height % 32 != 0: raise ValueError("row bit remap requires a display height divisible by 32") remapped = Image.new("RGB", image.size) for y in range(height): scan_row = y & 31 half_offset = y - scan_row physical_scan_row = _permute_low_five_bits(scan_row, bit_order) source_y = half_offset + physical_scan_row remapped.paste(image.crop((0, source_y, width, source_y + 1)), (0, y)) return remapped def _permute_low_five_bits(value: int, bit_order: tuple[int, int, int, int, int]) -> int: result = 0 for driver_bit, physical_bit in enumerate(bit_order): if value & (1 << driver_bit): result |= 1 << physical_bit return result