Initial commit
This commit is contained in:
		
						commit
						735e5de328
					
				
					 25 changed files with 894 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								image/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								image/__init__.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from .optimizer import optimize
 | 
			
		||||
 | 
			
		||||
__all__ = ["optimize"]
 | 
			
		||||
							
								
								
									
										210
									
								
								image/conversion.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								image/conversion.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,210 @@
 | 
			
		|||
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
 | 
			
		||||
							
								
								
									
										59
									
								
								image/optimizer.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								image/optimizer.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
from PIL import Image
 | 
			
		||||
import numpy as np
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
from .conversion import DEVICE_SPECS, ModelId, DeviceSpec
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def optimize(img: Image.Image, model: ModelId) -> Image.Image:
 | 
			
		||||
    """
 | 
			
		||||
    Optimize an image for a specific Gicisky device model.
 | 
			
		||||
    
 | 
			
		||||
    Args:
 | 
			
		||||
        img: Input PIL Image
 | 
			
		||||
        model: Device model identifier
 | 
			
		||||
        
 | 
			
		||||
    Returns:
 | 
			
		||||
        Image.Image: Optimized image
 | 
			
		||||
    """
 | 
			
		||||
    specs: DeviceSpec = DEVICE_SPECS.get(model)
 | 
			
		||||
    if not specs:
 | 
			
		||||
        raise ValueError(f"Unknown model: {model}")
 | 
			
		||||
    target_width, target_height = specs.size
 | 
			
		||||
 | 
			
		||||
    canvas = Image.new("RGB", (target_width, target_height), color="white")
 | 
			
		||||
    img_width, img_height = img.size
 | 
			
		||||
    scale = min(target_width / img_width, target_height / img_height)
 | 
			
		||||
    new_width = int(img_width * scale)
 | 
			
		||||
    new_height = int(img_height * scale)
 | 
			
		||||
 | 
			
		||||
    resized_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
 | 
			
		||||
    x_offset = (target_width - new_width) // 2
 | 
			
		||||
    y_offset = (target_height - new_height) // 2
 | 
			
		||||
 | 
			
		||||
    canvas.paste(resized_img, (x_offset, y_offset))
 | 
			
		||||
 | 
			
		||||
    palette = Image.new("P", (1, 1))
 | 
			
		||||
    colors = 3
 | 
			
		||||
    
 | 
			
		||||
    # Apply appropriate color conversion
 | 
			
		||||
    if specs.second_color:
 | 
			
		||||
        palette.putpalette([
 | 
			
		||||
            0, 0, 0,  # Black
 | 
			
		||||
            255, 255, 255,  # White
 | 
			
		||||
            255, 0, 0  # Red
 | 
			
		||||
        ])
 | 
			
		||||
    else:
 | 
			
		||||
        palette.putpalette([
 | 
			
		||||
            0, 0, 0,  # Black
 | 
			
		||||
            255, 255, 255,  # White
 | 
			
		||||
        ])
 | 
			
		||||
        colors = 2
 | 
			
		||||
 | 
			
		||||
    processed_img = canvas.quantize(method=Image.MEDIANCUT,
 | 
			
		||||
                                        colors=colors,
 | 
			
		||||
                                        kmeans=0,
 | 
			
		||||
                                        palette=palette)
 | 
			
		||||
    processed_img = processed_img.convert("RGB")
 | 
			
		||||
    
 | 
			
		||||
    return processed_img
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue