210 lines
6.1 KiB
Python
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
|