Initial Commit

This commit is contained in:
2026-02-13 12:22:06 -05:00
parent 43fdccf67b
commit 8fec41e3e6
612 changed files with 1313484 additions and 2 deletions

211
src/static_image_manager.py Normal file
View File

@@ -0,0 +1,211 @@
import logging
import os
import time
from typing import Optional, Tuple
from PIL import Image, ImageOps
import json
from .display_manager import DisplayManager
logger = logging.getLogger(__name__)
class StaticImageManager:
"""
Manager for displaying static images on the LED matrix.
Supports image scaling, transparency, and configurable display duration.
"""
def __init__(self, display_manager: DisplayManager, config: dict):
self.display_manager = display_manager
self.config = config.get('static_image', {})
# Configuration
self.enabled = self.config.get('enabled', False)
self.image_path = self.config.get('image_path', '')
# Get display duration from main display_durations block
self.display_duration = config.get('display', {}).get('display_durations', {}).get('static_image', 10)
self.fit_to_display = self.config.get('fit_to_display', True) # Auto-fit to display dimensions
self.preserve_aspect_ratio = self.config.get('preserve_aspect_ratio', True)
self.background_color = tuple(self.config.get('background_color', [0, 0, 0]))
# State
self.current_image = None
self.image_loaded = False
self.last_update_time = 0
# Load initial image if enabled
if self.enabled and self.image_path:
self._load_image()
def _load_image(self) -> bool:
"""
Load and process the image for display.
Returns True if successful, False otherwise.
"""
if not self.image_path or not os.path.exists(self.image_path):
logger.warning(f"[Static Image] Image file not found: {self.image_path}")
return False
try:
# Load the image
img = Image.open(self.image_path)
# Convert to RGBA to handle transparency
if img.mode != 'RGBA':
img = img.convert('RGBA')
# Get display dimensions
display_width = self.display_manager.matrix.width
display_height = self.display_manager.matrix.height
# Calculate target size - always fit to display while preserving aspect ratio
target_size = self._calculate_fit_size(img.size, (display_width, display_height))
# Resize image
if self.preserve_aspect_ratio:
img = img.resize(target_size, Image.Resampling.LANCZOS)
else:
img = img.resize((display_width, display_height), Image.Resampling.LANCZOS)
# Create display-sized canvas with background color
canvas = Image.new('RGB', (display_width, display_height), self.background_color)
# Calculate position to center the image
paste_x = (display_width - img.width) // 2
paste_y = (display_height - img.height) // 2
# Handle transparency by compositing
if img.mode == 'RGBA':
# Create a temporary image with the background color
temp_canvas = Image.new('RGB', (display_width, display_height), self.background_color)
temp_canvas.paste(img, (paste_x, paste_y), img)
canvas = temp_canvas
else:
canvas.paste(img, (paste_x, paste_y))
self.current_image = canvas
self.image_loaded = True
self.last_update_time = time.time()
logger.info(f"[Static Image] Successfully loaded and processed image: {self.image_path}")
logger.info(f"[Static Image] Original size: {Image.open(self.image_path).size}, "
f"Display size: {target_size}, Position: ({paste_x}, {paste_y})")
return True
except Exception as e:
logger.error(f"[Static Image] Error loading image {self.image_path}: {e}")
self.image_loaded = False
return False
def _calculate_fit_size(self, image_size: Tuple[int, int], display_size: Tuple[int, int]) -> Tuple[int, int]:
"""
Calculate the size to fit an image within display bounds while preserving aspect ratio.
"""
img_width, img_height = image_size
display_width, display_height = display_size
# Calculate scaling factor to fit within display
scale_x = display_width / img_width
scale_y = display_height / img_height
scale = min(scale_x, scale_y)
return (int(img_width * scale), int(img_height * scale))
def update(self):
"""
Update method - no continuous updates needed for static images.
"""
pass
def display(self, force_clear: bool = False):
"""
Display the static image on the LED matrix.
"""
if not self.enabled or not self.image_loaded or not self.current_image:
if self.enabled:
logger.warning("[Static Image] Manager enabled but no image loaded")
return
# Clear display if requested
if force_clear:
self.display_manager.clear()
# Set the image on the display manager
self.display_manager.image = self.current_image.copy()
# Update the display
self.display_manager.update_display()
logger.debug(f"[Static Image] Displayed image: {self.image_path}")
def set_image_path(self, image_path: str) -> bool:
"""
Set a new image path and load it.
Returns True if successful, False otherwise.
"""
self.image_path = image_path
return self._load_image()
def set_fit_to_display(self, fit_to_display: bool):
"""
Set the fit to display option and reload the image.
"""
self.fit_to_display = fit_to_display
if self.image_path:
self._load_image()
logger.info(f"[Static Image] Fit to display set to: {self.fit_to_display}")
def set_display_duration(self, duration: int):
"""
Set the display duration in seconds.
"""
self.display_duration = max(1, duration) # Minimum 1 second
logger.info(f"[Static Image] Display duration set to: {self.display_duration} seconds")
def set_background_color(self, color: Tuple[int, int, int]):
"""
Set the background color and reload the image.
"""
self.background_color = color
if self.image_path:
self._load_image()
logger.info(f"[Static Image] Background color set to: {self.background_color}")
def get_image_info(self) -> dict:
"""
Get information about the currently loaded image.
"""
if not self.image_loaded or not self.current_image:
return {"loaded": False}
return {
"loaded": True,
"path": self.image_path,
"display_size": self.current_image.size,
"fit_to_display": self.fit_to_display,
"display_duration": self.display_duration,
"background_color": self.background_color
}
def reload_image(self) -> bool:
"""
Reload the current image.
"""
if not self.image_path:
logger.warning("[Static Image] No image path set for reload")
return False
return self._load_image()
def is_enabled(self) -> bool:
"""
Check if the manager is enabled.
"""
return self.enabled
def get_display_duration(self) -> int:
"""
Get the display duration in seconds.
"""
return self.display_duration