""" EU-Utility - Icon Extractor Integration Integrates TGA icon extraction from Lemontropia Suite's Icon Extractor. Provides icon cache management and TGA to PNG conversion. """ import struct from pathlib import Path from io import BytesIO from typing import Optional, Tuple, Any try: from PIL import Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False print("[IconExtractor] PIL not available. Install: pip install Pillow") class TGAReader: """Read TGA files from Entropia Universe cache.""" TGA_TYPES = { 0: 'NO_IMAGE', 1: 'COLOR_MAPPED', 2: 'RGB', 3: 'GRAYSCALE', 9: 'RLE_COLOR_MAPPED', 10: 'RLE_RGB', 11: 'RLE_GRAYSCALE', } def __init__(self, file_path: Path): self.file_path = file_path self.header = None self.width = 0 self.height = 0 self.pixels = None def read(self) -> Optional[Any]: # Returns Optional[Image.Image] """Read TGA file and return PIL Image.""" if not PIL_AVAILABLE: return None # Import Image here to avoid NameError from PIL import Image try: with open(self.file_path, 'rb') as f: data = f.read() return self._parse_tga(data) except Exception as e: print(f"[TGAReader] Error reading {self.file_path}: {e}") return None def _parse_tga(self, data: bytes) -> Optional[Image.Image]: """Parse TGA data.""" if len(data) < 18: return None # Read header id_length = data[0] color_map_type = data[1] image_type = data[2] # Color map spec (5 bytes) color_map_origin = struct.unpack(' Optional[Any]: # Optional[Image.Image] """Read RGB pixel data.""" if not PIL_AVAILABLE: return None from PIL import Image if depth not in (24, 32): return None bytes_per_pixel = depth // 8 expected_size = self.width * self.height * bytes_per_pixel if rle: data = self._decode_rle(data, bytes_per_pixel) if len(data) < expected_size: return None # Convert BGR to RGB if bytes_per_pixel == 3: mode = 'RGB' pixels = bytearray() for i in range(0, len(data), 3): if i + 2 < len(data): pixels.extend([data[i+2], data[i+1], data[i]]) # BGR to RGB else: mode = 'RGBA' pixels = bytearray() for i in range(0, len(data), 4): if i + 3 < len(data): pixels.extend([data[i+2], data[i+1], data[i], data[i+3]]) # BGRA to RGBA try: img = Image.frombytes(mode, (self.width, self.height), bytes(pixels)) # Flip vertically (TGA stores bottom-to-top) img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM) return img except Exception as e: print(f"[TGAReader] Error creating image: {e}") return None def _read_grayscale(self, data: bytes, rle: bool) -> Optional[Any]: # Optional[Image.Image] """Read grayscale pixel data.""" if not PIL_AVAILABLE: return None from PIL import Image expected_size = self.width * self.height if rle: data = self._decode_rle(data, 1) if len(data) < expected_size: return None try: img = Image.frombytes('L', (self.width, self.height), data[:expected_size]) img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM) return img except Exception as e: print(f"[TGAReader] Error creating grayscale image: {e}") return None def _decode_rle(self, data: bytes, bytes_per_pixel: int) -> bytes: """Decode RLE compressed data.""" result = bytearray() i = 0 while i < len(data): header = data[i] i += 1 if header >= 128: # Run-length packet count = (header - 128) + 1 pixel = data[i:i + bytes_per_pixel] i += bytes_per_pixel for _ in range(count): result.extend(pixel) else: # Raw packet count = header + 1 for _ in range(count): result.extend(data[i:i + bytes_per_pixel]) i += bytes_per_pixel return bytes(result) class IconCacheManager: """Manages EU icon cache extraction and storage.""" def __init__(self, cache_dir: str = "assets/eu_icons"): self.cache_dir = Path(cache_dir) self.cache_dir.mkdir(parents=True, exist_ok=True) self.icon_index = {} self._load_index() def _load_index(self): """Load icon index.""" index_file = self.cache_dir / "index.json" if index_file.exists(): try: import json with open(index_file, 'r') as f: self.icon_index = json.load(f) except: self.icon_index = {} def _save_index(self): """Save icon index.""" import json index_file = self.cache_dir / "index.json" with open(index_file, 'w') as f: json.dump(self.icon_index, f, indent=2) def extract_from_tga(self, tga_path: Path, icon_name: str) -> Optional[Path]: """Extract icon from TGA file and save as PNG.""" if not PIL_AVAILABLE: return None reader = TGAReader(tga_path) img = reader.read() if img: # Save as PNG output_path = self.cache_dir / f"{icon_name}.png" img.save(output_path, 'PNG') # Update index self.icon_index[icon_name] = { 'source': str(tga_path), 'size': (reader.width, reader.height), 'cached': str(output_path) } self._save_index() return output_path return None def get_icon(self, icon_name: str, size: Tuple[int, int] = (32, 32)) -> Optional[Path]: """Get icon path, extracting from TGA if needed.""" # Check cache cached = self.cache_dir / f"{icon_name}.png" if cached.exists(): return cached # Check if we have source TGA if icon_name in self.icon_index: source = Path(self.icon_index[icon_name]['source']) if source.exists(): return self.extract_from_tga(source, icon_name) return None def scan_cache_directory(self, eu_cache_path: Path): """Scan EU cache directory for TGA icons.""" if not eu_cache_path.exists(): return print(f"[IconCache] Scanning {eu_cache_path}...") found = 0 for tga_file in eu_cache_path.rglob("*.tga"): # Try to extract icon_name = tga_file.stem if self.extract_from_tga(tga_file, icon_name): found += 1 print(f"[IconCache] Extracted {found} icons") def list_icons(self) -> list: """List all available cached icons.""" icons = [] for png_file in self.cache_dir.glob("*.png"): icons.append(png_file.stem) return sorted(icons) # Singleton instance _icon_cache = None def get_icon_cache() -> IconCacheManager: """Get global icon cache instance.""" global _icon_cache if _icon_cache is None: _icon_cache = IconCacheManager() return _icon_cache