gicisky/image/conversion.py
2025-11-01 00:16:33 +01:00

210 lines
6.1 KiB
Python

import dataclasses
from typing import Tuple, Dict, List
import numpy as np
from PIL import Image
@dataclasses.dataclass
class DeviceSpec:
model: str
size: Tuple[int, int]
mirror: bool = False
rotation: bool = False
second_color: bool = True
tft: bool = False
compression: bool = False
max_voltage: float = 2.9
min_voltage: float = 2.2
ModelId = int
DEVICE_SPECS: Dict[ModelId, DeviceSpec] = {
0x0B: DeviceSpec(
model="EPD 2.1\" BWR",
size=(250, 122)
),
0x33: DeviceSpec(
model="EPD 2.9\" BWR",
size=(296, 128),
mirror=True,
max_voltage=3.0
),
0x4B: DeviceSpec(
model="EPD 4.2\" BWR",
size=(400, 300),
rotation=True,
max_voltage=3.0
),
0x2B: DeviceSpec(
model="EPD 7.5\" BWR",
size=(800, 480),
mirror=True,
rotation=True,
compression=True,
max_voltage=3.0
),
0xA0: DeviceSpec(
model="TFT 2.1\" BW",
size=(250, 132),
second_color=False,
tft=True
)
}
def convert_to_gicisky_bytes(img: Image.Image, model: ModelId, lum_threshold: int = 128, red_threshold: int = 170) -> bytes:
"""
Process image according to Gicisky device specifications.
Usage with image.optimize is recommended.
Args:
img: PIL Image to process
model: Device model identifier
lum_threshold: Luminance threshold for black/white conversion
red_threshold: Threshold for red color detection
Returns:
bytes: Processed image data in Gicisky format
"""
# Get device specifications
specs = DEVICE_SPECS.get(model)
if not specs:
raise ValueError("Unknown model")
width, height = specs.size
# Resize image to device dimensions
img = img.convert("RGB").resize((width, height), Image.Resampling.LANCZOS)
# Apply TFT transformation if enabled
if specs.tft:
# Resize to half width and double height
img = img.resize((width // 2, height * 2), Image.Resampling.LANCZOS)
width, height = img.size
# Apply mirroring if enabled
if specs.mirror:
img = img.transpose(Image.FLIP_TOP_BOTTOM)
# Convert to numpy array
arr = np.array(img)
# Process pixels - column-major order
byte_data, red_byte_data = [], []
current_byte, current_red_byte = 0, 0
bit_position = 7
for x in range(width):
for y in range(height):
r, g, b = arr[y, x] # Note: numpy uses [y, x] indexing
luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
# Apply thresholding based on compression setting
if specs.compression:
# When compression is enabled, dark pixels set bits
if luminance < lum_threshold:
current_byte |= (1 << bit_position)
else:
# When compression is disabled, light pixels set bits
if luminance > lum_threshold:
current_byte |= (1 << bit_position)
# Red color detection
if r > red_threshold and g < red_threshold:
current_red_byte |= (1 << bit_position)
bit_position -= 1
if bit_position < 0:
byte_data.append(current_byte)
red_byte_data.append(current_red_byte)
current_byte, current_red_byte = 0, 0
bit_position = 7
# Handle remaining bits
if bit_position != 7:
byte_data.append(current_byte)
red_byte_data.append(current_red_byte)
# Apply compression if enabled
if specs.compression:
byte_data_compressed = _apply_compression(byte_data, red_byte_data, width, height, specs.second_color)
else:
# Simple concatenation when compression is disabled
byte_data_compressed = byte_data[:]
if specs.second_color:
byte_data_compressed.extend(red_byte_data)
return bytes(byte_data_compressed)
def _apply_compression(byte_data: List[int], red_byte_data: List[int],
width: int, height: int, second_color: bool) -> List[int]:
"""
Apply compression algorithm
Args:
byte_data: Black/white pixel data
red_byte_data: Red pixel data
width: Image width
height: Image height
second_color: Whether to include second color (red) data
Returns:
List[int]: Compressed byte data
"""
byte_data_compressed = [0x00, 0x00, 0x00, 0x00] # Header
byte_per_line = height // 8
current_pos = 0
# Process black/white data
for i in range(width):
# Add line header
byte_data_compressed.extend([
0x75,
byte_per_line + 7,
byte_per_line,
0x00,
0x00,
0x00,
0x00
])
# Add pixel data for this line
for b in range(byte_per_line):
if current_pos < len(byte_data):
byte_data_compressed.append(byte_data[current_pos])
else:
byte_data_compressed.append(0x00) # Padding
current_pos += 1
# Process red data if enabled
if second_color:
current_pos = 0
for i in range(width):
# Add line header
byte_data_compressed.extend([
0x75,
byte_per_line + 7,
byte_per_line,
0x00,
0x00,
0x00,
0x00
])
# Add pixel data for this line
for b in range(byte_per_line):
if current_pos < len(red_byte_data):
byte_data_compressed.append(red_byte_data[current_pos])
else:
byte_data_compressed.append(0x00) # Padding
current_pos += 1
# Update header with total length
total_length = len(byte_data_compressed)
byte_data_compressed[0] = total_length & 0xff
byte_data_compressed[1] = (total_length >> 8) & 0xff
byte_data_compressed[2] = (total_length >> 16) & 0xff
byte_data_compressed[3] = (total_length >> 24) & 0xff
return byte_data_compressed