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
 |