"""Image conversion utilities for ATOMS3 MacroPad.""" import math import os import random import shutil from PIL import Image, ImageTk from .constants import SCREEN_W, SCREEN_H, IMAGES_DIR def ensure_images_dir(): os.makedirs(IMAGES_DIR, exist_ok=True) def generate_gradient_image() -> str: """Generate a random linear gradient PNG, save it to images dir, return its path.""" ensure_images_dir() c1 = (random.randint(30, 220), random.randint(30, 220), random.randint(30, 220)) c2 = (random.randint(30, 220), random.randint(30, 220), random.randint(30, 220)) angle = math.radians(random.randint(0, 359)) cos_a = math.cos(angle) sin_a = math.sin(angle) size = SCREEN_W # 128x128 to match device display img = Image.new("RGB", (size, size)) pixels = img.load() for y in range(size): for x in range(size): nx = (x / (size - 1)) * 2 - 1 ny = (y / (size - 1)) * 2 - 1 t = max(0.0, min(1.0, (nx * cos_a + ny * sin_a + 1) / 2)) pixels[x, y] = ( int(c1[0] + (c2[0] - c1[0]) * t), int(c1[1] + (c2[1] - c1[1]) * t), int(c1[2] + (c2[2] - c1[2]) * t), ) token = "%08x" % random.getrandbits(32) path = os.path.join(IMAGES_DIR, f"gradient_{token}.png") img.save(path) return path def copy_image_to_appdata(image_path: str) -> str: """Copy an image to the AppData images folder, return new path.""" ensure_images_dir() basename = os.path.basename(image_path) # Hash-suffix the filename to avoid collisions between same-named imports name, ext = os.path.splitext(basename) import hashlib h = hashlib.md5(open(image_path, "rb").read()).hexdigest()[:8] dest = os.path.join(IMAGES_DIR, f"{name}_{h}{ext}") if not os.path.exists(dest): shutil.copy2(image_path, dest) return dest def convert_to_rgb565(image_path: str) -> bytes: """Convert image to 128x128 RGB565 big-endian bytes (32768 bytes).""" img = Image.open(image_path).convert("RGB") img = img.resize((SCREEN_W, SCREEN_H), Image.LANCZOS) data = bytearray(SCREEN_W * SCREEN_H * 2) pixels = img.load() for y in range(SCREEN_H): for x in range(SCREEN_W): r, g, b = pixels[x, y] rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) offset = (y * SCREEN_W + x) * 2 data[offset] = (rgb565 >> 8) & 0xFF # MSB data[offset + 1] = rgb565 & 0xFF # LSB return bytes(data) def create_thumbnail(image_path: str, size: tuple = (64, 64)): """Create a tkinter-compatible thumbnail from an image file.""" img = Image.open(image_path).convert("RGB") img.thumbnail(size, Image.LANCZOS) return ImageTk.PhotoImage(img) def create_preview(image_path: str, size: tuple = (128, 128)): """Larger preview image for the properties panel.""" img = Image.open(image_path).convert("RGB") img = img.resize(size, Image.LANCZOS) return ImageTk.PhotoImage(img)