diff --git a/src/opendisplay/encoding/bitplanes.py b/src/opendisplay/encoding/bitplanes.py index f697079..6100f3b 100644 --- a/src/opendisplay/encoding/bitplanes.py +++ b/src/opendisplay/encoding/bitplanes.py @@ -37,35 +37,18 @@ def encode_bitplanes( if image.mode != "P": raise ValueError(f"Expected palette image, got {image.mode}") - pixels = np.array(image) - height, width = pixels.shape - - # Calculate output size (1bpp, 8 pixels per byte) - bytes_per_row = (width + 7) // 8 - plane1 = bytearray(bytes_per_row * height) # BW plane - plane2 = bytearray(bytes_per_row * height) # R/Y plane + pixels = np.asarray(image) # Palette mapping: # Index 0 = Black -> BW=0, R/Y=0 # Index 1 = White -> BW=1, R/Y=0 # Index 2 = Red/Yellow -> BW=0, R/Y=1 + # packbits(axis=1) zero-pads each row to a byte boundary (8 pixels per byte, + # MSB first). + plane1 = np.packbits(pixels == 1, axis=1).tobytes() # BW plane + plane2 = np.packbits(pixels == 2, axis=1).tobytes() # R/Y plane - for y in range(height): - for x in range(width): - byte_idx = y * bytes_per_row + x // 8 - bit_idx = 7 - (x % 8) # MSB first - - palette_idx = pixels[y, x] - - if palette_idx == 1: - # White - set BW plane - plane1[byte_idx] |= 1 << bit_idx - elif palette_idx == 2: - # Red/Yellow - set R/Y plane - plane2[byte_idx] |= 1 << bit_idx - # else: palette_idx == 0 (black) - both planes stay 0 - - return bytes(plane1), bytes(plane2) + return plane1, plane2 def encode_gray4_bitplanes( @@ -93,20 +76,12 @@ def encode_gray4_bitplanes( if image.mode != "P": raise ValueError(f"Expected palette image, got {image.mode}") - pixels = np.array(image) - height, width = pixels.shape - bytes_per_row = (width + 7) // 8 - plane0 = bytearray(bytes_per_row * height) - plane1 = bytearray(bytes_per_row * height) - - for y in range(height): - for x in range(width): - byte_idx = y * bytes_per_row + x // 8 - bit = 1 << (7 - (x % 8)) # MSB first - code = gray_codes[int(pixels[y, x]) & 0x03] - if code & 0x01: - plane0[byte_idx] |= bit - if code & 0x02: - plane1[byte_idx] |= bit - - return bytes(plane0), bytes(plane1) + pixels = np.asarray(image) + + # Map each pixel's dither level (index 0..3) through the panel's gray-code + # table to a 2-bit stored code, then split into two 1-bit planes. + codes = np.asarray(gray_codes, dtype=np.uint8)[pixels & 0x03] + plane0 = np.packbits(codes & 0x01, axis=1).tobytes() + plane1 = np.packbits(codes & 0x02, axis=1).tobytes() + + return plane0, plane1 diff --git a/src/opendisplay/encoding/images.py b/src/opendisplay/encoding/images.py index c4a8813..e721f2a 100644 --- a/src/opendisplay/encoding/images.py +++ b/src/opendisplay/encoding/images.py @@ -115,22 +115,11 @@ def encode_1bpp(image: Image.Image) -> bytes: if image.mode != "P": raise ValueError(f"Expected palette image, got {image.mode}") - pixels = np.array(image) - height, width = pixels.shape - - # Calculate output size (round up to byte boundary) - bytes_per_row = (width + 7) // 8 - output = bytearray(bytes_per_row * height) - - for y in range(height): - for x in range(width): - byte_idx = y * bytes_per_row + x // 8 - bit_idx = 7 - (x % 8) # MSB first - - if pixels[y, x] > 0: # Non-zero palette index = white - output[byte_idx] |= 1 << bit_idx + pixels = np.asarray(image) - return bytes(output) + # Any non-zero palette index = white (bit set). packbits(axis=1) zero-pads + # each row to a byte boundary, matching the per-row layout above. + return np.packbits(pixels > 0, axis=1).tobytes() def encode_2bpp(image: Image.Image, codes: tuple[int, int, int, int] | None = None) -> bytes: @@ -151,25 +140,21 @@ def encode_2bpp(image: Image.Image, codes: tuple[int, int, int, int] | None = No if image.mode != "P": raise ValueError(f"Expected palette image, got {image.mode}") - pixels = np.array(image) + pixels = np.asarray(image) height, width = pixels.shape - # Calculate output size (round up to 4-pixel boundary) - bytes_per_row = (width + 3) // 4 - output = bytearray(bytes_per_row * height) - - for y in range(height): - for x in range(width): - byte_idx = y * bytes_per_row + x // 4 - pixel_in_byte = x % 4 - bit_shift = (3 - pixel_in_byte) * 2 # MSB first - - palette_idx = pixels[y, x] & 0x03 # 2-bit value - if codes is not None: - palette_idx = codes[palette_idx] - output[byte_idx] |= palette_idx << bit_shift - - return bytes(output) + # Mask to 2 bits, remap through the panel code table if given, zero-pad the + # width to a multiple of 4 (matches the per-row byte boundary), then pack + # 4 pixels per byte MSB-first. + p = (pixels & 0x03).astype(np.uint8) + if codes is not None: + p = np.asarray(codes, dtype=np.uint8)[p] + pad = (-width) % 4 + if pad: + p = np.pad(p, ((0, 0), (0, pad))) + p = p.reshape(height, -1, 4) + packed = (p[:, :, 0] << 6) | (p[:, :, 1] << 4) | (p[:, :, 2] << 2) | p[:, :, 3] + return packed.astype(np.uint8).tobytes() def encode_4bpp(image: Image.Image, bwgbry_mapping: bool = False) -> bytes: @@ -193,32 +178,22 @@ def encode_4bpp(image: Image.Image, bwgbry_mapping: bool = False) -> bytes: if image.mode != "P": raise ValueError(f"Expected palette image, got {image.mode}") - pixels = np.array(image) + pixels = np.asarray(image) height, width = pixels.shape - # BWGBRY firmware color mapping (Spectra 6 display) - # Palette indices to firmware values: 0→0, 1→1, 2→2, 3→3, 4→5, 5→6 - BWGBRY_MAP = {0: 0, 1: 1, 2: 2, 3: 3, 4: 5, 5: 6} - - # Calculate output size (round up to 2-pixel boundary) - bytes_per_row = (width + 1) // 2 - output = bytearray(bytes_per_row * height) - - for y in range(height): - for x in range(width): - byte_idx = y * bytes_per_row + x // 2 - - palette_idx = pixels[y, x] & 0x0F # 4-bit value - - # Apply BWGBRY mapping if needed - if bwgbry_mapping: - palette_idx = BWGBRY_MAP.get(palette_idx, 0) - - if x % 2 == 0: - # High nibble - output[byte_idx] |= palette_idx << 4 - else: - # Low nibble - output[byte_idx] |= palette_idx - - return bytes(output) + idx = (pixels & 0x0F).astype(np.uint8) + + # BWGBRY firmware color mapping (Spectra 6 display): palette indices to + # firmware values 0→0, 1→1, 2→2, 3→3, 4→5, 5→6, everything else → 0 + # (preserving the previous dict.get(..., 0) default). + if bwgbry_mapping: + lut = np.array([0, 1, 2, 3, 5, 6] + [0] * 10, dtype=np.uint8) + idx = lut[idx] + + # Zero-pad width to an even number (matches the per-row byte boundary), + # then pack 2 pixels per byte, high nibble first. + if width & 1: + idx = np.pad(idx, ((0, 0), (0, 1))) + idx = idx.reshape(height, -1, 2) + packed = (idx[:, :, 0] << 4) | idx[:, :, 1] + return packed.astype(np.uint8).tobytes()