feat: add TGA Icon Converter for game cache files
- New module: modules/tga_converter.py - Reads Truevision TGA format from game cache - Converts to PNG with proper color handling (BGR->RGB) - Auto-detects cache folder location - Batch conversion support - Handles alpha channels - New dialog: ui/tga_converter_dialog.py - Visual interface for TGA conversion - Shows list of cached .tga files - Progress tracking during conversion - One-click convert all - Added to Tools → Computer Vision → TGA Icon Converter (Ctrl+T) This is much more efficient than extracting icons from screenshots! The game already has the icons in cache as .tga files.
This commit is contained in:
parent
adc76e42cc
commit
acca0d3491
|
|
@ -0,0 +1,392 @@
|
||||||
|
"""
|
||||||
|
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('<H', header[8:10])[0]
|
||||||
|
y_origin = struct.unpack('<H', header[10:12])[0]
|
||||||
|
width = struct.unpack('<H', header[12:14])[0]
|
||||||
|
height = struct.unpack('<H', header[14:16])[0]
|
||||||
|
pixel_depth = header[16]
|
||||||
|
image_descriptor = header[17]
|
||||||
|
|
||||||
|
# Check if valid TGA
|
||||||
|
if width == 0 or height == 0:
|
||||||
|
logger.warning(f"Invalid TGA dimensions: {filepath}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
has_alpha = (pixel_depth == 32) or (image_descriptor & 0x0F > 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()
|
||||||
|
|
@ -121,6 +121,7 @@ from ui.session_history import SessionHistoryDialog
|
||||||
from ui.gallery_dialog import GalleryDialog, ScreenshotCapture
|
from ui.gallery_dialog import GalleryDialog, ScreenshotCapture
|
||||||
from ui.settings_dialog import SettingsDialog
|
from ui.settings_dialog import SettingsDialog
|
||||||
from ui.inventory_scanner_dialog import InventoryScannerDialog
|
from ui.inventory_scanner_dialog import InventoryScannerDialog
|
||||||
|
from ui.tga_converter_dialog import TGAConverterDialog
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Screenshot Hotkey Integration
|
# Screenshot Hotkey Integration
|
||||||
|
|
@ -761,6 +762,12 @@ class MainWindow(QMainWindow):
|
||||||
inventory_scan_action.triggered.connect(self.on_inventory_scan)
|
inventory_scan_action.triggered.connect(self.on_inventory_scan)
|
||||||
vision_menu.addAction(inventory_scan_action)
|
vision_menu.addAction(inventory_scan_action)
|
||||||
|
|
||||||
|
# TGA Icon Converter
|
||||||
|
tga_convert_action = QAction("🔧 &TGA Icon Converter", self)
|
||||||
|
tga_convert_action.setShortcut("Ctrl+T")
|
||||||
|
tga_convert_action.triggered.connect(self.on_tga_convert)
|
||||||
|
vision_menu.addAction(tga_convert_action)
|
||||||
|
|
||||||
# View menu
|
# View menu
|
||||||
view_menu = menubar.addMenu("&View")
|
view_menu = menubar.addMenu("&View")
|
||||||
|
|
||||||
|
|
@ -1913,6 +1920,15 @@ class MainWindow(QMainWindow):
|
||||||
self.log_error("Vision", f"Failed to open inventory scanner: {e}")
|
self.log_error("Vision", f"Failed to open inventory scanner: {e}")
|
||||||
QMessageBox.warning(self, "Error", f"Could not open Inventory Scanner: {e}")
|
QMessageBox.warning(self, "Error", f"Could not open Inventory Scanner: {e}")
|
||||||
|
|
||||||
|
def on_tga_convert(self):
|
||||||
|
"""Open TGA Icon Converter dialog."""
|
||||||
|
try:
|
||||||
|
dialog = TGAConverterDialog(self)
|
||||||
|
dialog.exec()
|
||||||
|
except Exception as e:
|
||||||
|
self.log_error("Vision", f"Failed to open TGA converter: {e}")
|
||||||
|
QMessageBox.warning(self, "Error", f"Could not open TGA Converter: {e}")
|
||||||
|
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
# Settings Management
|
# Settings Management
|
||||||
# ========================================================================
|
# ========================================================================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
"""
|
||||||
|
Lemontropia Suite - TGA Icon Converter Dialog
|
||||||
|
UI for converting Entropia Universe cached .tga icons to PNG.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||||
|
QLabel, QPushButton, QListWidget, QListWidgetItem,
|
||||||
|
QGroupBox, QProgressBar, QTextEdit, QFileDialog,
|
||||||
|
QMessageBox, QSpinBox, QCheckBox
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QPixmap
|
||||||
|
|
||||||
|
from modules.tga_converter import TGAConverter
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TGAConvertWorker(QThread):
|
||||||
|
"""Background worker for TGA conversion."""
|
||||||
|
|
||||||
|
progress_update = pyqtSignal(str)
|
||||||
|
file_converted = pyqtSignal(str, str) # tga_name, png_path
|
||||||
|
conversion_complete = pyqtSignal(int, int) # success_count, total_count
|
||||||
|
conversion_error = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, converter: TGAConverter):
|
||||||
|
super().__init__()
|
||||||
|
self.converter = converter
|
||||||
|
self._is_running = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the conversion."""
|
||||||
|
try:
|
||||||
|
# Find cache folder
|
||||||
|
self.progress_update.emit("Finding cache folder...")
|
||||||
|
cache_path = self.converter.find_cache_folder()
|
||||||
|
|
||||||
|
if not cache_path:
|
||||||
|
self.conversion_error.emit("Cache folder not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get list of TGA files
|
||||||
|
tga_files = list(cache_path.glob("*.tga"))
|
||||||
|
total = len(tga_files)
|
||||||
|
success = 0
|
||||||
|
|
||||||
|
self.progress_update.emit(f"Found {total} TGA files")
|
||||||
|
|
||||||
|
for i, tga_path in enumerate(tga_files):
|
||||||
|
if not self._is_running:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.progress_update.emit(f"[{i+1}/{total}] Converting: {tga_path.name}")
|
||||||
|
|
||||||
|
png_path = self.converter.convert_tga_to_png(tga_path)
|
||||||
|
if png_path:
|
||||||
|
success += 1
|
||||||
|
self.file_converted.emit(tga_path.name, str(png_path))
|
||||||
|
|
||||||
|
self.conversion_complete.emit(success, total)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.conversion_error.emit(str(e))
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the conversion."""
|
||||||
|
self._is_running = False
|
||||||
|
|
||||||
|
|
||||||
|
class TGAConverterDialog(QDialog):
|
||||||
|
"""
|
||||||
|
Dialog for converting Entropia Universe TGA icons to PNG.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("🔧 TGA Icon Converter")
|
||||||
|
self.setMinimumSize(700, 600)
|
||||||
|
self.resize(800, 700)
|
||||||
|
|
||||||
|
self.converter = TGAConverter()
|
||||||
|
self.convert_worker: Optional[TGAConvertWorker] = None
|
||||||
|
self.converted_files = []
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
self._apply_dark_theme()
|
||||||
|
|
||||||
|
# Auto-scan on open
|
||||||
|
self._scan_cache()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
"""Setup the dialog UI."""
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(15, 15, 15, 15)
|
||||||
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
header = QLabel("🔧 TGA Icon Converter")
|
||||||
|
header.setStyleSheet("font-size: 18px; font-weight: bold; color: #4caf50;")
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
desc = QLabel(
|
||||||
|
"Convert Entropia Universe cached .tga icon files to PNG format.\n"
|
||||||
|
"The game stores item icons in the cache folder as .tga files."
|
||||||
|
)
|
||||||
|
desc.setStyleSheet("color: #888; padding: 5px;")
|
||||||
|
desc.setWordWrap(True)
|
||||||
|
layout.addWidget(desc)
|
||||||
|
|
||||||
|
# Cache folder info
|
||||||
|
cache_group = QGroupBox("📁 Cache Folder")
|
||||||
|
cache_layout = QVBoxLayout(cache_group)
|
||||||
|
|
||||||
|
self.cache_path_label = QLabel("Searching...")
|
||||||
|
self.cache_path_label.setStyleSheet("font-family: Consolas; color: #888;")
|
||||||
|
cache_layout.addWidget(self.cache_path_label)
|
||||||
|
|
||||||
|
cache_buttons = QHBoxLayout()
|
||||||
|
|
||||||
|
self.scan_btn = QPushButton("🔍 Scan Cache")
|
||||||
|
self.scan_btn.clicked.connect(self._scan_cache)
|
||||||
|
cache_buttons.addWidget(self.scan_btn)
|
||||||
|
|
||||||
|
browse_cache_btn = QPushButton("Browse...")
|
||||||
|
browse_cache_btn.clicked.connect(self._browse_cache_folder)
|
||||||
|
cache_buttons.addWidget(browse_cache_btn)
|
||||||
|
|
||||||
|
cache_buttons.addStretch()
|
||||||
|
cache_layout.addLayout(cache_buttons)
|
||||||
|
|
||||||
|
layout.addWidget(cache_group)
|
||||||
|
|
||||||
|
# TGA Files list
|
||||||
|
files_group = QGroupBox("📄 TGA Files Found")
|
||||||
|
files_layout = QVBoxLayout(files_group)
|
||||||
|
|
||||||
|
self.files_count_label = QLabel("No files found")
|
||||||
|
files_layout.addWidget(self.files_count_label)
|
||||||
|
|
||||||
|
self.files_list = QListWidget()
|
||||||
|
self.files_list.setMaximumHeight(150)
|
||||||
|
files_layout.addWidget(self.files_list)
|
||||||
|
|
||||||
|
layout.addWidget(files_group)
|
||||||
|
|
||||||
|
# Output settings
|
||||||
|
output_group = QGroupBox("💾 Output Settings")
|
||||||
|
output_layout = QHBoxLayout(output_group)
|
||||||
|
|
||||||
|
output_layout.addWidget(QLabel("Output folder:"))
|
||||||
|
|
||||||
|
self.output_path_label = QLabel(str(self.converter.output_dir))
|
||||||
|
self.output_path_label.setStyleSheet("font-family: Consolas; color: #888;")
|
||||||
|
output_layout.addWidget(self.output_path_label, 1)
|
||||||
|
|
||||||
|
output_browse = QPushButton("Browse...")
|
||||||
|
output_browse.clicked.connect(self._browse_output_folder)
|
||||||
|
output_layout.addWidget(output_browse)
|
||||||
|
|
||||||
|
layout.addWidget(output_group)
|
||||||
|
|
||||||
|
# Convert button
|
||||||
|
self.convert_btn = QPushButton("🚀 Convert All to PNG")
|
||||||
|
self.convert_btn.setMinimumHeight(50)
|
||||||
|
self.convert_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #0d47a1;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #1565c0; }
|
||||||
|
QPushButton:disabled { background-color: #333; }
|
||||||
|
""")
|
||||||
|
self.convert_btn.clicked.connect(self._start_conversion)
|
||||||
|
layout.addWidget(self.convert_btn)
|
||||||
|
|
||||||
|
# Progress
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setRange(0, 0) # Indeterminate
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
self.status_label = QLabel("Ready")
|
||||||
|
self.status_label.setStyleSheet("color: #888; padding: 5px;")
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
# Results
|
||||||
|
results_group = QGroupBox("✅ Converted Files")
|
||||||
|
results_layout = QVBoxLayout(results_group)
|
||||||
|
|
||||||
|
self.results_list = QListWidget()
|
||||||
|
results_layout.addWidget(self.results_list)
|
||||||
|
|
||||||
|
results_buttons = QHBoxLayout()
|
||||||
|
|
||||||
|
open_folder_btn = QPushButton("📂 Open Output Folder")
|
||||||
|
open_folder_btn.clicked.connect(self._open_output_folder)
|
||||||
|
results_buttons.addWidget(open_folder_btn)
|
||||||
|
|
||||||
|
results_buttons.addStretch()
|
||||||
|
results_layout.addLayout(results_buttons)
|
||||||
|
|
||||||
|
layout.addWidget(results_group)
|
||||||
|
|
||||||
|
# Close button
|
||||||
|
close_btn = QPushButton("Close")
|
||||||
|
close_btn.clicked.connect(self.accept)
|
||||||
|
layout.addWidget(close_btn)
|
||||||
|
|
||||||
|
def _scan_cache(self):
|
||||||
|
"""Scan for TGA files in cache."""
|
||||||
|
self.files_list.clear()
|
||||||
|
self.files_count_label.setText("Scanning...")
|
||||||
|
|
||||||
|
cache_path = self.converter.find_cache_folder()
|
||||||
|
|
||||||
|
if cache_path:
|
||||||
|
self.cache_path_label.setText(str(cache_path))
|
||||||
|
|
||||||
|
tga_files = list(cache_path.glob("*.tga"))
|
||||||
|
self.files_count_label.setText(f"Found {len(tga_files)} TGA files")
|
||||||
|
|
||||||
|
for tga_file in tga_files:
|
||||||
|
item = QListWidgetItem(tga_file.name)
|
||||||
|
# Try to get TGA info
|
||||||
|
tga_info = self.converter.read_tga_header(tga_file)
|
||||||
|
if tga_info:
|
||||||
|
item.setToolTip(f"{tga_info.width}x{tga_info.height}, {tga_info.pixel_depth}bpp")
|
||||||
|
self.files_list.addItem(item)
|
||||||
|
|
||||||
|
self.convert_btn.setEnabled(len(tga_files) > 0)
|
||||||
|
else:
|
||||||
|
self.cache_path_label.setText("❌ Not found")
|
||||||
|
self.files_count_label.setText("Cache folder not found")
|
||||||
|
self.convert_btn.setEnabled(False)
|
||||||
|
|
||||||
|
def _browse_cache_folder(self):
|
||||||
|
"""Browse for cache folder."""
|
||||||
|
dir_path = QFileDialog.getExistingDirectory(
|
||||||
|
self,
|
||||||
|
"Select Entropia Universe Cache Folder",
|
||||||
|
str(Path.home() / "Documents" / "Entropia Universe"),
|
||||||
|
QFileDialog.Option.ShowDirsOnly
|
||||||
|
)
|
||||||
|
|
||||||
|
if dir_path:
|
||||||
|
self.converter._cache_path = Path(dir_path)
|
||||||
|
self._scan_cache()
|
||||||
|
|
||||||
|
def _browse_output_folder(self):
|
||||||
|
"""Browse for output folder."""
|
||||||
|
dir_path = QFileDialog.getExistingDirectory(
|
||||||
|
self,
|
||||||
|
"Select Output Folder",
|
||||||
|
str(self.converter.output_dir),
|
||||||
|
QFileDialog.Option.ShowDirsOnly
|
||||||
|
)
|
||||||
|
|
||||||
|
if dir_path:
|
||||||
|
self.converter.output_dir = Path(dir_path)
|
||||||
|
self.output_path_label.setText(dir_path)
|
||||||
|
|
||||||
|
def _start_conversion(self):
|
||||||
|
"""Start TGA to PNG conversion."""
|
||||||
|
if self.convert_worker and self.convert_worker.isRunning():
|
||||||
|
QMessageBox.warning(self, "Busy", "Conversion already in progress")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.convert_btn.setEnabled(False)
|
||||||
|
self.progress_bar.setVisible(True)
|
||||||
|
self.results_list.clear()
|
||||||
|
self.converted_files = []
|
||||||
|
|
||||||
|
# Start worker
|
||||||
|
self.convert_worker = TGAConvertWorker(self.converter)
|
||||||
|
self.convert_worker.progress_update.connect(self._on_progress)
|
||||||
|
self.convert_worker.file_converted.connect(self._on_file_converted)
|
||||||
|
self.convert_worker.conversion_complete.connect(self._on_conversion_complete)
|
||||||
|
self.convert_worker.conversion_error.connect(self._on_conversion_error)
|
||||||
|
self.convert_worker.start()
|
||||||
|
|
||||||
|
def _on_progress(self, message: str):
|
||||||
|
"""Handle progress update."""
|
||||||
|
self.status_label.setText(message)
|
||||||
|
|
||||||
|
def _on_file_converted(self, tga_name: str, png_path: str):
|
||||||
|
"""Handle file converted."""
|
||||||
|
self.converted_files.append(png_path)
|
||||||
|
item = QListWidgetItem(f"✅ {tga_name} → {Path(png_path).name}")
|
||||||
|
self.results_list.addItem(item)
|
||||||
|
|
||||||
|
def _on_conversion_complete(self, success: int, total: int):
|
||||||
|
"""Handle conversion complete."""
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
self.convert_btn.setEnabled(True)
|
||||||
|
self.status_label.setText(f"Complete: {success}/{total} files converted")
|
||||||
|
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Conversion Complete",
|
||||||
|
f"Successfully converted {success} of {total} files\n\n"
|
||||||
|
f"Output: {self.converter.output_dir}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_conversion_error(self, error: str):
|
||||||
|
"""Handle conversion error."""
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
self.convert_btn.setEnabled(True)
|
||||||
|
self.status_label.setText(f"Error: {error}")
|
||||||
|
|
||||||
|
QMessageBox.critical(self, "Error", error)
|
||||||
|
|
||||||
|
def _open_output_folder(self):
|
||||||
|
"""Open output folder in file explorer."""
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
|
||||||
|
output_path = str(self.converter.output_dir)
|
||||||
|
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
os.startfile(output_path)
|
||||||
|
elif platform.system() == "Darwin": # macOS
|
||||||
|
import subprocess
|
||||||
|
subprocess.run(["open", output_path])
|
||||||
|
else: # Linux
|
||||||
|
import subprocess
|
||||||
|
subprocess.run(["xdg-open", output_path])
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Handle dialog close."""
|
||||||
|
if self.convert_worker and self.convert_worker.isRunning():
|
||||||
|
self.convert_worker.stop()
|
||||||
|
self.convert_worker.wait(1000)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def _apply_dark_theme(self):
|
||||||
|
"""Apply dark theme."""
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QDialog {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
QGroupBox {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
QListWidget {
|
||||||
|
background-color: #252525;
|
||||||
|
border: 1px solid #444;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
background-color: #0d47a1;
|
||||||
|
border: 1px solid #1565c0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #1565c0;
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
Loading…
Reference in New Issue