# image_processor.py import os import math import logging from typing import Optional, List, Dict, Tuple try: from PIL import Image, ImageDraw, ImageFont PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False # Definisci classi dummy se PIL non c'è, per evitare errori altrove class Image: pass class ImageDraw: pass class ImageFont: pass logging.error( "Pillow library (PIL) not found. Image processing features will be disabled." ) # Costanti per disegno (possono essere spostate in config se necessario) TILE_TEXT_COLOR = "white" TILE_TEXT_BG_COLOR = "black" # Sfondo opaco PLACEHOLDER_COLOR = (128, 128, 128) # Grigio per placeholder RGB TILE_BORDER_COLOR = "red" TILE_BORDER_WIDTH = 2 # Spessore bordo griglia # Carica font (una sola volta) DEFAULT_FONT = None if PIL_AVAILABLE: try: # Prova diversi font comuni font_paths = [ "arial.ttf", "LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", ] for font_path in font_paths: try: DEFAULT_FONT = ImageFont.truetype( font_path, 12 ) # Aumenta dimensione font logging.info(f"Loaded font: {font_path}") break except IOError: continue # Prova il prossimo if DEFAULT_FONT is None: DEFAULT_FONT = ImageFont.load_default() logging.warning("Common system fonts not found, using default PIL font.") except Exception as e: logging.error(f"Error loading font: {e}") DEFAULT_FONT = ImageFont.load_default() if PIL_AVAILABLE else None def add_overlay_info( image: Optional[Image.Image], tile_name: str, source_text: str = "NASADEM" ) -> Optional[Image.Image]: """ Draws tile name and source information onto a PIL Image object. Args: image (Optional[Image.Image]): The PIL Image object to draw on. If None, does nothing. tile_name (str): The base name of the tile (e.g., 'n45e007'). source_text (str): Text indicating the data source. Returns: Optional[Image.Image]: The modified image object, or None if input was None or PIL unavailable. """ if not PIL_AVAILABLE or image is None: return None try: # Assicurati sia modificabile (RGB o RGBA) if image.mode not in ["RGB", "RGBA"]: image = image.convert("RGB") draw = ImageDraw.Draw(image) text = f"{tile_name.upper()}\nSource: {source_text}" font = DEFAULT_FONT if DEFAULT_FONT else ImageFont.load_default() # Calcola dimensioni testo per posizionamento try: # textbbox è più preciso ma richiede Pillow >= 8.0.0 text_bbox = draw.textbbox((0, 0), text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] except AttributeError: # Fallback per versioni Pillow più vecchie text_width, text_height = draw.textsize(text, font=font) margin = 5 text_x = image.width - text_width - margin text_y = image.height - text_height - margin # Disegna sfondo e testo bg_coords = [ text_x - 2, text_y - 2, text_x + text_width + 2, text_y + text_height + 2, ] draw.rectangle(bg_coords, fill=TILE_TEXT_BG_COLOR) draw.text((text_x, text_y), text, fill=TILE_TEXT_COLOR, font=font) return image except Exception as e: logging.error( f"Error adding overlay to image for {tile_name}: {e}", exc_info=True ) return image # Restituisci immagine originale in caso di errore def load_prepare_single_browse( image_path: Optional[str], tile_name: str ) -> Optional[Image.Image]: """ Loads a browse image from path, adds overlay info. Args: image_path (Optional[str]): Path to the browse image file. tile_name (str): Base name of the tile (e.g., 'n45e007'). Returns: Optional[Image.Image]: Prepared PIL Image object or None if loading/processing fails. """ if not PIL_AVAILABLE or not image_path or not os.path.exists(image_path): return None try: logging.debug(f"Loading browse image: {image_path}") img = Image.open(image_path) img_with_overlay = add_overlay_info(img, tile_name) return img_with_overlay except Exception as e: logging.error( f"Failed to load or prepare browse image {image_path}: {e}", exc_info=True ) return None def create_composite_area_image( tile_info_list: List[Dict], target_tile_size: Optional[Tuple[int, int]] = None ) -> Optional[Image.Image]: """ Creates a composite image by mosaicking available browse images for an area. Args: tile_info_list (List[Dict]): List of dictionaries from ElevationManager.get_area_tile_info. target_tile_size (Optional[Tuple[int, int]]): If provided, resize each tile to this size. Otherwise, uses max dimensions found. Returns: Optional[Image.Image]: Composite PIL Image object or None if no tiles or error. """ if not PIL_AVAILABLE or not tile_info_list: logging.warning( "Cannot create composite image: PIL unavailable or no tile info provided." ) return None logging.info(f"Creating composite image for {len(tile_info_list)} tile area.") # Estrai coordinate min/max per determinare la griglia lats = [info["latitude_coord"] for info in tile_info_list] lons = [info["longitude_coord"] for info in tile_info_list] if not lats or not lons: return None # Lista vuota? min_lat, max_lat = min(lats), max(lats) min_lon, max_lon = min(lons), max(lons) num_lat_tiles = max_lat - min_lat + 1 num_lon_tiles = max_lon - min_lon + 1 # Carica immagini e determina dimensioni massime (o usa target) tile_images: Dict[Tuple[int, int], Optional[Image.Image]] = {} max_w, max_h = 0, 0 loaded_image_count = 0 for info in tile_info_list: lat, lon = info["latitude_coord"], info["longitude_coord"] key = (lat, lon) img_path = info.get("browse_image_path") img = None if img_path and os.path.exists(img_path): try: img = Image.open(img_path).convert("RGB") # Carica come RGB tile_images[key] = img if not target_tile_size: # Se non forziamo dimensione, trova max max_w = max(max_w, img.width) max_h = max(max_h, img.height) loaded_image_count += 1 except Exception as e: logging.warning(f"Could not load browse image {img_path}: {e}") tile_images[key] = None # Segna come non caricata else: tile_images[key] = None # Non disponibile if loaded_image_count == 0: logging.warning("No browse images available for the specified area.") # Potremmo restituire None o un'immagine placeholder unica? Per ora None. return None # Imposta dimensione cella: target o massima trovata tile_w = target_tile_size[0] if target_tile_size else max_w tile_h = target_tile_size[1] if target_tile_size else max_h if tile_w <= 0 or tile_h <= 0: logging.error( "Invalid target tile size or no valid images found to determine size." ) return None # Dimensione non valida # Crea immagine composita total_width = num_lon_tiles * tile_w total_height = num_lat_tiles * tile_h logging.info( f"Creating composite canvas: {total_width}x{total_height} ({num_lon_tiles}x{num_lat_tiles} tiles of {tile_w}x{tile_h})" ) composite_img = Image.new("RGB", (total_width, total_height), PLACEHOLDER_COLOR) draw = ImageDraw.Draw(composite_img) # Incolla tile, disegna griglia e testo for r, lat_coord in enumerate( range(max_lat, min_lat - 1, -1) ): # Y cresce verso il basso (Nord in alto) for c, lon_coord in enumerate( range(min_lon, max_lon + 1) ): # X cresce verso destra (Ovest a sinistra) key = (lat_coord, lon_coord) img = tile_images.get(key) paste_x = c * tile_w paste_y = r * tile_h if img: # Ridimensiona se necessario if img.width != tile_w or img.height != tile_h: img = img.resize((tile_w, tile_h), Image.Resampling.LANCZOS) composite_img.paste(img, (paste_x, paste_y)) # Disegna bordo draw.rectangle( [paste_x, paste_y, paste_x + tile_w - 1, paste_y + tile_h - 1], outline=TILE_BORDER_COLOR, width=TILE_BORDER_WIDTH, ) # Aggiungi etichetta (riutilizza logica) tile_name = f"{'N' if lat_coord >= 0 else 'S'}{abs(lat_coord):02d}{'E' if lon_coord >= 0 else 'W'}{abs(lon_coord):03d}" add_overlay_info( composite_img.crop( (paste_x, paste_y, paste_x + tile_w, paste_y + tile_h) ), # Disegna su un crop temporaneo? No, direttamente sulla composita tile_name, source_text="", ) # Passa empty source? O solo nome tile? # Disegna nome tile direttamente sulla composita font = DEFAULT_FONT if DEFAULT_FONT else ImageFont.load_default() text = tile_name try: text_bbox = draw.textbbox((0, 0), text, font=font) text_w = text_bbox[2] - text_bbox[0] text_h = text_bbox[3] - text_bbox[1] except AttributeError: text_w, text_h = draw.textsize(text, font=font) # Fallback margin = 3 text_pos_x = paste_x + tile_w - text_w - margin text_pos_y = paste_y + tile_h - text_h - margin bg_coords = [ text_pos_x - 1, text_pos_y - 1, text_pos_x + text_w + 1, text_pos_y + text_h + 1, ] draw.rectangle(bg_coords, fill=TILE_TEXT_BG_COLOR) draw.text((text_pos_x, text_pos_y), text, fill=TILE_TEXT_COLOR, font=font) logging.info("Composite image created successfully.") return composite_img