506 lines
19 KiB
Python
506 lines
19 KiB
Python
"""
|
|
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, QComboBox
|
|
)
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
from PyQt6.QtGui import QPixmap
|
|
|
|
from modules.tga_converter import TGAConverter
|
|
from modules.ai_upscaler import AIIconUpscaler, REALESRGAN_AVAILABLE
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TGAConvertWorker(QThread):
|
|
"""Background worker for TGA conversion."""
|
|
|
|
progress_update = pyqtSignal(str)
|
|
file_converted = pyqtSignal(str, str) # tga_name, output_path
|
|
conversion_complete = pyqtSignal(int, int) # success_count, total_count
|
|
conversion_error = pyqtSignal(str)
|
|
|
|
def __init__(self, converter: TGAConverter, cache_path: Optional[Path] = None,
|
|
canvas_size=None, upscale: bool = False, upscale_method: str = 'nearest'):
|
|
super().__init__()
|
|
self.converter = converter
|
|
self.cache_path = cache_path
|
|
self.canvas_size = canvas_size
|
|
self.upscale = upscale
|
|
self.upscale_method = upscale_method
|
|
self._is_running = True
|
|
|
|
def run(self):
|
|
"""Run the conversion."""
|
|
try:
|
|
# Use provided cache path or find it
|
|
if self.cache_path and self.cache_path.exists():
|
|
cache_path = self.cache_path
|
|
self.progress_update.emit(f"Using cache folder: {cache_path}")
|
|
else:
|
|
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
|
|
|
|
# Initialize AI upscaler if needed
|
|
ai_upscaler = None
|
|
if self.upscale_method == 'ai' and REALESRGAN_AVAILABLE:
|
|
self.progress_update.emit("Loading AI upscaler (Real-ESRGAN)...")
|
|
ai_upscaler = AIIconUpscaler(device='cpu')
|
|
if not ai_upscaler.is_available():
|
|
self.progress_update.emit("AI model not found, falling back to HQ4x")
|
|
self.upscale_method = 'hq4x'
|
|
|
|
canvas_info = f" ({self.canvas_size[0]}x{self.canvas_size[1]} canvas)" if self.canvas_size else ""
|
|
self.progress_update.emit(f"Found {total} TGA files to convert{canvas_info}")
|
|
|
|
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}")
|
|
|
|
# Handle AI upscaling separately
|
|
if self.upscale_method == 'ai' and ai_upscaler and ai_upscaler.is_available():
|
|
output_path = self._convert_with_ai(tga_path, ai_upscaler)
|
|
else:
|
|
output_path = self.converter.convert_tga_to_png(
|
|
tga_path,
|
|
canvas_size=self.canvas_size,
|
|
upscale=True,
|
|
upscale_method=self.upscale_method
|
|
)
|
|
|
|
if output_path:
|
|
success += 1
|
|
self.file_converted.emit(tga_path.name, str(output_path))
|
|
|
|
self.conversion_complete.emit(success, total)
|
|
|
|
except Exception as e:
|
|
self.conversion_error.emit(str(e))
|
|
|
|
def _convert_with_ai(self, tga_path: Path, ai_upscaler: AIIconUpscaler) -> Optional[Path]:
|
|
"""Convert a single TGA file using AI upscaling."""
|
|
try:
|
|
from PIL import Image
|
|
|
|
# Load TGA
|
|
image = Image.open(tga_path)
|
|
if image.mode != 'RGBA':
|
|
image = image.convert('RGBA')
|
|
|
|
# AI upscale (4x)
|
|
self.progress_update.emit(f" AI upscaling {tga_path.name}...")
|
|
upscaled = ai_upscaler.upscale(image, scale=4)
|
|
|
|
if upscaled is None:
|
|
return None
|
|
|
|
# Apply canvas if requested
|
|
if self.canvas_size:
|
|
upscaled = self.converter._apply_canvas(
|
|
upscaled, self.canvas_size, upscale=False
|
|
)
|
|
|
|
# Save
|
|
output_path = self.converter.output_dir / f"{tga_path.stem}.png"
|
|
upscaled.save(output_path, 'PNG')
|
|
|
|
return output_path
|
|
|
|
except Exception as e:
|
|
logger.error(f"AI conversion failed: {e}")
|
|
return None
|
|
|
|
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.\n"
|
|
"Icons are centered on a canvas of your chosen size with transparent background."
|
|
)
|
|
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)
|
|
|
|
# Canvas size settings
|
|
canvas_group = QGroupBox("🖼️ Canvas Size")
|
|
canvas_layout = QHBoxLayout(canvas_group)
|
|
|
|
canvas_layout.addWidget(QLabel("Size:"))
|
|
self.canvas_combo = QComboBox()
|
|
self.canvas_combo.addItem("Original (no canvas)", None)
|
|
self.canvas_combo.addItem("64x64", (64, 64))
|
|
self.canvas_combo.addItem("128x128", (128, 128))
|
|
self.canvas_combo.addItem("256x256", (256, 256))
|
|
self.canvas_combo.addItem("280x280", (280, 280))
|
|
self.canvas_combo.addItem("320x320", (320, 320))
|
|
self.canvas_combo.addItem("512x512", (512, 512))
|
|
self.canvas_combo.setCurrentIndex(5) # Default to 320x320
|
|
canvas_layout.addWidget(self.canvas_combo)
|
|
|
|
canvas_layout.addSpacing(20)
|
|
|
|
canvas_layout.addWidget(QLabel("Upscale:"))
|
|
self.upscale_method_combo = QComboBox()
|
|
self.upscale_method_combo.addItem("❌ No Upscaling", "none")
|
|
self.upscale_method_combo.addItem("Sharp Pixels (NEAREST)", "nearest")
|
|
self.upscale_method_combo.addItem("Smooth (HQ4x-style)", "hq4x")
|
|
self.upscale_method_combo.addItem("Photorealistic (LANCZOS)", "lanczos")
|
|
|
|
# Add AI option if Real-ESRGAN is available
|
|
if REALESRGAN_AVAILABLE:
|
|
self.upscale_method_combo.addItem("🤖 AI Enhanced (Real-ESRGAN)", "ai")
|
|
|
|
self.upscale_method_combo.setToolTip(
|
|
"None: Keep original size, just center on canvas\n"
|
|
"NEAREST: Sharp pixel edges (best for pixel art)\n"
|
|
"HQ4x: Smooth but keeps details (best for game icons)\n"
|
|
"LANCZOS: Very smooth (best for photos)\n"
|
|
"AI: Neural network upscaling (best quality, requires model)"
|
|
)
|
|
canvas_layout.addWidget(self.upscale_method_combo)
|
|
|
|
canvas_layout.addStretch()
|
|
layout.addWidget(canvas_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...")
|
|
|
|
# Use manually set path if available, otherwise find it
|
|
if self.converter._cache_path and self.converter._cache_path.exists():
|
|
cache_path = self.converter._cache_path
|
|
else:
|
|
cache_path = self.converter.find_cache_folder()
|
|
|
|
if cache_path:
|
|
self.cache_path_label.setText(str(cache_path))
|
|
|
|
# Use rglob to search ALL subfolders recursively
|
|
tga_files = list(cache_path.rglob("*.tga"))
|
|
self.files_count_label.setText(f"Found {len(tga_files)} TGA files")
|
|
|
|
for tga_file in tga_files:
|
|
# Show relative path in list
|
|
try:
|
|
display_name = str(tga_file.relative_to(cache_path))
|
|
except:
|
|
display_name = tga_file.name
|
|
|
|
item = QListWidgetItem(display_name)
|
|
item.setData(Qt.ItemDataRole.UserRole, str(tga_file))
|
|
|
|
# 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)
|
|
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 = []
|
|
|
|
# Get canvas settings
|
|
canvas_size = self.canvas_combo.currentData()
|
|
upscale_method = self.upscale_method_combo.currentData()
|
|
|
|
# Start worker with settings
|
|
cache_path = self.converter._cache_path if self.converter._cache_path else None
|
|
self.convert_worker = TGAConvertWorker(
|
|
self.converter, cache_path, canvas_size,
|
|
upscale=True, upscale_method=upscale_method
|
|
)
|
|
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;
|
|
}
|
|
""") |