""" Lemontropia Suite - TGA Icon Converter Convert Entropia Universe cached .tga icons to PNG format. The game stores item icons as .tga files in the cache folder. This module converts them to PNG for easier use. """ import struct import logging from pathlib import Path from typing import Optional, List, Tuple, Dict from dataclasses import dataclass import numpy as np try: from PIL import Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False Image = None logger = logging.getLogger(__name__) @dataclass class TGAFile: """Represents a TGA file with metadata.""" filepath: Path width: int height: int pixel_depth: int has_alpha: bool def __str__(self) -> str: return f"TGA({self.width}x{self.height}, {self.pixel_depth}bpp, alpha={self.has_alpha})" class TGAConverter: """ Converter for Entropia Universe TGA icon files. Usage: converter = TGAConverter() # Convert single file png_path = converter.convert_tga_to_png("item_icon.tga") # Batch convert all cached icons results = converter.convert_cache_folder() """ # Common Entropia Universe cache locations DEFAULT_CACHE_PATHS = [ Path.home() / "Documents" / "Entropia Universe" / "cache" / "icons", Path.home() / "Documents" / "Entropia Universe" / "cache", Path("C:") / "ProgramData" / "Entropia Universe" / "cache" / "icons", Path("C:") / "Program Files (x86)" / "Entropia Universe" / "cache" / "icons", ] def __init__(self, output_dir: Optional[Path] = None): """ Initialize TGA converter. Args: output_dir: Directory for converted PNG files. Default: Documents/Entropia Universe/Icons/ """ if output_dir is None: output_dir = Path.home() / "Documents" / "Entropia Universe" / "Icons" self.output_dir = output_dir self.output_dir.mkdir(parents=True, exist_ok=True) self._cache_path: Optional[Path] = None if not PIL_AVAILABLE: logger.warning("PIL/Pillow not available. Install with: pip install Pillow") def find_cache_folder(self) -> Optional[Path]: """ Find the Entropia Universe icon cache folder. Returns: Path to cache folder or None if not found """ # Check default locations for path in self.DEFAULT_CACHE_PATHS: if path.exists(): logger.info(f"Found cache folder: {path}") self._cache_path = path return path # Try to find by looking for .tga files logger.info("Searching for .tga files...") docs_path = Path.home() / "Documents" / "Entropia Universe" if docs_path.exists(): for tga_file in docs_path.rglob("*.tga"): cache_path = tga_file.parent logger.info(f"Found cache folder via search: {cache_path}") self._cache_path = cache_path return cache_path logger.warning("Could not find Entropia Universe cache folder") return None def read_tga_header(self, filepath: Path) -> Optional[TGAFile]: """ Read TGA file header. Args: filepath: Path to .tga file Returns: TGAFile with metadata or None if invalid """ try: with open(filepath, 'rb') as f: # Read TGA header (18 bytes) header = f.read(18) if len(header) < 18: logger.warning(f"Invalid TGA file (too small): {filepath}") return None # Unpack header # Format: id_length, color_map_type, image_type, color_map_spec(5), # x_origin(2), y_origin(2), width(2), height(2), pixel_depth, descriptor id_length = header[0] color_map_type = header[1] image_type = header[2] # Skip color map specification (5 bytes) x_origin = struct.unpack(' 0) return TGAFile( filepath=filepath, width=width, height=height, pixel_depth=pixel_depth, has_alpha=has_alpha ) except Exception as e: logger.error(f"Error reading TGA header: {e}") return None def read_tga_pixels(self, tga_file: TGAFile) -> Optional[np.ndarray]: """ Read pixel data from TGA file. Args: tga_file: TGAFile with metadata Returns: Numpy array of pixels (H, W, C) or None """ try: with open(tga_file.filepath, 'rb') as f: # Skip header f.seek(18) # Skip ID field if present header = f.read(18) id_length = header[0] if len(header) >= 18 else 0 if id_length > 0: f.seek(18 + id_length) # Calculate pixel data size bytes_per_pixel = tga_file.pixel_depth // 8 pixel_data_size = tga_file.width * tga_file.height * bytes_per_pixel # Read pixel data pixel_data = f.read(pixel_data_size) if len(pixel_data) < pixel_data_size: logger.warning(f"Incomplete TGA pixel data: {tga_file.filepath}") return None # Convert to numpy array pixels = np.frombuffer(pixel_data, dtype=np.uint8) # Reshape based on format if bytes_per_pixel == 4: # BGRA format pixels = pixels.reshape((tga_file.height, tga_file.width, 4)) # Convert BGRA to RGBA pixels = pixels[:, :, [2, 1, 0, 3]] elif bytes_per_pixel == 3: # BGR format pixels = pixels.reshape((tga_file.height, tga_file.width, 3)) # Convert BGR to RGB pixels = pixels[:, :, [2, 1, 0]] elif bytes_per_pixel == 2: # 16-bit format - convert to RGB pixels = pixels.view(np.uint16).reshape((tga_file.height, tga_file.width)) # Simple conversion (not accurate but works) pixels = np.stack([ ((pixels >> 10) & 0x1F) << 3, ((pixels >> 5) & 0x1F) << 3, (pixels & 0x1F) << 3 ], axis=-1).astype(np.uint8) else: logger.warning(f"Unsupported pixel depth: {tga_file.pixel_depth}") return None # Flip vertically (TGA stores bottom-to-top) pixels = np.flipud(pixels) return pixels except Exception as e: logger.error(f"Error reading TGA pixels: {e}") return None def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None) -> Optional[Path]: """ Convert a TGA file to PNG. Args: tga_path: Path to .tga file output_name: Optional output filename (without extension) Returns: Path to converted PNG file or None if failed """ if not PIL_AVAILABLE: logger.error("PIL/Pillow required for TGA to PNG conversion") return None try: # Read TGA tga_file = self.read_tga_header(tga_path) if not tga_file: return None logger.info(f"Converting: {tga_path.name} ({tga_file})") pixels = self.read_tga_pixels(tga_file) if pixels is None: return None # Create PIL Image if pixels.shape[2] == 4: # RGBA image = Image.fromarray(pixels, 'RGBA') else: # RGB image = Image.fromarray(pixels, 'RGB') # Determine output path if output_name is None: output_name = tga_path.stem png_path = self.output_dir / f"{output_name}.png" # Save as PNG image.save(png_path, 'PNG') logger.info(f"Saved: {png_path}") return png_path except Exception as e: logger.error(f"Error converting {tga_path}: {e}") return None def convert_cache_folder(self, cache_path: Optional[Path] = None) -> Dict[str, Path]: """ Convert all TGA files in cache folder to PNG. Args: cache_path: Path to cache folder (auto-detect if None) Returns: Dictionary mapping TGA filenames to PNG paths """ results = {} if cache_path is None: cache_path = self.find_cache_folder() if not cache_path or not cache_path.exists(): logger.error("Cache folder not found") return results # Find all TGA files tga_files = list(cache_path.glob("*.tga")) if not tga_files: logger.warning(f"No .tga files found in: {cache_path}") return results logger.info(f"Found {len(tga_files)} TGA files to convert") # Convert each file for i, tga_path in enumerate(tga_files, 1): logger.info(f"[{i}/{len(tga_files)}] Converting: {tga_path.name}") png_path = self.convert_tga_to_png(tga_path) if png_path: results[tga_path.name] = png_path logger.info(f"Converted {len(results)}/{len(tga_files)} files successfully") logger.info(f"Output directory: {self.output_dir}") return results def get_conversion_summary(self) -> str: """Get a summary of available TGA files and conversion status.""" cache_path = self.find_cache_folder() if not cache_path: return "āŒ Cache folder not found" tga_files = list(cache_path.glob("*.tga")) png_files = list(self.output_dir.glob("*.png")) lines = [ "šŸ“ TGA Icon Cache Summary", "", f"Cache location: {cache_path}", f"TGA files found: {len(tga_files)}", f"PNG files converted: {len(png_files)}", f"Output directory: {self.output_dir}", "", ] if tga_files: lines.append("Sample TGA files:") for tga in tga_files[:5]: tga_info = self.read_tga_header(tga) if tga_info: lines.append(f" • {tga.name} ({tga_info.width}x{tga_info.height})") else: lines.append(f" • {tga.name} (invalid)") if len(tga_files) > 5: lines.append(f" ... and {len(tga_files) - 5} more") return "\n".join(lines) # Convenience functions def convert_tga_to_png(tga_path: str, output_dir: Optional[str] = None) -> Optional[str]: """Quick convert single TGA file.""" converter = TGAConverter(Path(output_dir) if output_dir else None) result = converter.convert_tga_to_png(Path(tga_path)) return str(result) if result else None def convert_all_cache_icons(output_dir: Optional[str] = None) -> List[str]: """Convert all cached icons.""" converter = TGAConverter(Path(output_dir) if output_dir else None) results = converter.convert_cache_folder() return [str(path) for path in results.values()] def main(): """CLI interface for TGA conversion.""" import sys print("šŸ”§ Lemontropia Suite - TGA Icon Converter") print("=" * 50) converter = TGAConverter() # Show summary print(converter.get_conversion_summary()) print() # Ask to convert response = input("\nConvert all TGA files to PNG? (y/n): ") if response.lower() == 'y': results = converter.convert_cache_folder() print(f"\nāœ… Converted {len(results)} files to:") print(converter.output_dir) else: print("Cancelled.") if __name__ == "__main__": main()