feat: add WebP output format support for TGA conversion

- Updated TGAConverter.convert_tga_to_png() to support 'webp' format
- High quality WebP encoding (quality=95, method=6)
- Added format selector in UI (PNG or WebP)
- Convert button text updates based on selected format
- WebP is better for web uploads: smaller files, better quality

WebP advantages:
- 25-35% smaller file size than PNG
- Better quality at same file size
- Alpha channel support
- Widely supported in modern browsers
This commit is contained in:
LemonNexus 2026-02-11 16:17:31 +00:00
parent 14bac40fdf
commit ac334ac4a8
2 changed files with 68 additions and 31 deletions

View File

@ -263,16 +263,18 @@ class TGAConverter:
logger.error(f"Error reading TGA pixels: {e}") logger.error(f"Error reading TGA pixels: {e}")
return None return None
def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None) -> Optional[Path]: def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None,
output_format: str = 'png') -> Optional[Path]:
""" """
Convert a TGA file to PNG. Convert a TGA file to PNG or WebP.
Args: Args:
tga_path: Path to .tga file tga_path: Path to .tga file
output_name: Optional output filename (without extension) output_name: Optional output filename (without extension)
output_format: 'png' or 'webp'
Returns: Returns:
Path to converted PNG file or None if failed Path to converted file or None if failed
""" """
if not PIL_AVAILABLE: if not PIL_AVAILABLE:
logger.error("PIL/Pillow required for TGA to PNG conversion") logger.error("PIL/Pillow required for TGA to PNG conversion")
@ -292,15 +294,21 @@ class TGAConverter:
else: else:
image = image.convert('RGBA') image = image.convert('RGBA')
# Determine output path # Determine output path and format
if output_name is None: if output_name is None:
output_name = tga_path.stem output_name = tga_path.stem
png_path = self.output_dir / f"{output_name}.png" # Save as PNG or WebP
image.save(png_path, 'PNG') if output_format.lower() == 'webp':
output_path = self.output_dir / f"{output_name}.webp"
# WebP with good quality and lossless option for sharp icons
image.save(output_path, 'WEBP', quality=95, method=6)
else:
output_path = self.output_dir / f"{output_name}.png"
image.save(output_path, 'PNG')
logger.info(f"Converted (PIL): {tga_path.name} -> {png_path.name}") logger.info(f"Converted (PIL): {tga_path.name} -> {output_path.name}")
return png_path return output_path
except Exception as pil_error: except Exception as pil_error:
logger.debug(f"PIL failed, trying manual: {pil_error}") logger.debug(f"PIL failed, trying manual: {pil_error}")
@ -322,29 +330,36 @@ class TGAConverter:
else: else:
image = Image.fromarray(pixels, 'RGB') image = Image.fromarray(pixels, 'RGB')
# Determine output path # Determine output path and format
if output_name is None: if output_name is None:
output_name = tga_path.stem output_name = tga_path.stem
png_path = self.output_dir / f"{output_name}.png" # Save as PNG or WebP
image.save(png_path, 'PNG') if output_format.lower() == 'webp':
output_path = self.output_dir / f"{output_name}.webp"
image.save(output_path, 'WEBP', quality=95, method=6)
else:
output_path = self.output_dir / f"{output_name}.png"
image.save(output_path, 'PNG')
logger.info(f"Converted (manual): {tga_path.name} -> {png_path.name}") logger.info(f"Converted (manual): {tga_path.name} -> {output_path.name}")
return png_path return output_path
except Exception as e: except Exception as e:
logger.error(f"Error converting {tga_path}: {e}") logger.error(f"Error converting {tga_path}: {e}")
return None return None
def convert_cache_folder(self, cache_path: Optional[Path] = None) -> Dict[str, Path]: def convert_cache_folder(self, cache_path: Optional[Path] = None,
output_format: str = 'png') -> Dict[str, Path]:
""" """
Convert all TGA files in cache folder to PNG. Convert all TGA files in cache folder to PNG or WebP.
Args: Args:
cache_path: Path to cache folder (auto-detect if None) cache_path: Path to cache folder (auto-detect if None)
output_format: 'png' or 'webp'
Returns: Returns:
Dictionary mapping TGA filenames to PNG paths Dictionary mapping TGA filenames to output paths
""" """
results = {} results = {}
@ -362,14 +377,14 @@ class TGAConverter:
logger.warning(f"No .tga files found in: {cache_path}") logger.warning(f"No .tga files found in: {cache_path}")
return results return results
logger.info(f"Found {len(tga_files)} TGA files to convert") logger.info(f"Found {len(tga_files)} TGA files to convert to {output_format.upper()}")
# Convert each file # Convert each file
for i, tga_path in enumerate(tga_files, 1): for i, tga_path in enumerate(tga_files, 1):
logger.info(f"[{i}/{len(tga_files)}] Converting: {tga_path.name}") logger.info(f"[{i}/{len(tga_files)}] Converting: {tga_path.name}")
png_path = self.convert_tga_to_png(tga_path) output_path = self.convert_tga_to_png(tga_path, output_format=output_format)
if png_path: if output_path:
results[tga_path.name] = png_path results[tga_path.name] = output_path
logger.info(f"Converted {len(results)}/{len(tga_files)} files successfully") logger.info(f"Converted {len(results)}/{len(tga_files)} files successfully")
logger.info(f"Output directory: {self.output_dir}") logger.info(f"Output directory: {self.output_dir}")

View File

@ -11,7 +11,7 @@ from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QPushButton, QListWidget, QListWidgetItem, QLabel, QPushButton, QListWidget, QListWidgetItem,
QGroupBox, QProgressBar, QTextEdit, QFileDialog, QGroupBox, QProgressBar, QTextEdit, QFileDialog,
QMessageBox, QSpinBox, QCheckBox QMessageBox, QSpinBox, QCheckBox, QComboBox
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QPixmap from PyQt6.QtGui import QPixmap
@ -25,14 +25,16 @@ class TGAConvertWorker(QThread):
"""Background worker for TGA conversion.""" """Background worker for TGA conversion."""
progress_update = pyqtSignal(str) progress_update = pyqtSignal(str)
file_converted = pyqtSignal(str, str) # tga_name, png_path file_converted = pyqtSignal(str, str) # tga_name, output_path
conversion_complete = pyqtSignal(int, int) # success_count, total_count conversion_complete = pyqtSignal(int, int) # success_count, total_count
conversion_error = pyqtSignal(str) conversion_error = pyqtSignal(str)
def __init__(self, converter: TGAConverter, cache_path: Optional[Path] = None): def __init__(self, converter: TGAConverter, cache_path: Optional[Path] = None,
output_format: str = 'png'):
super().__init__() super().__init__()
self.converter = converter self.converter = converter
self.cache_path = cache_path self.cache_path = cache_path
self.output_format = output_format
self._is_running = True self._is_running = True
def run(self): def run(self):
@ -55,7 +57,7 @@ class TGAConvertWorker(QThread):
total = len(tga_files) total = len(tga_files)
success = 0 success = 0
self.progress_update.emit(f"Found {total} TGA files") self.progress_update.emit(f"Found {total} TGA files to convert to {self.output_format.upper()}")
for i, tga_path in enumerate(tga_files): for i, tga_path in enumerate(tga_files):
if not self._is_running: if not self._is_running:
@ -63,10 +65,10 @@ class TGAConvertWorker(QThread):
self.progress_update.emit(f"[{i+1}/{total}] Converting: {tga_path.name}") self.progress_update.emit(f"[{i+1}/{total}] Converting: {tga_path.name}")
png_path = self.converter.convert_tga_to_png(tga_path) output_path = self.converter.convert_tga_to_png(tga_path, output_format=self.output_format)
if png_path: if output_path:
success += 1 success += 1
self.file_converted.emit(tga_path.name, str(png_path)) self.file_converted.emit(tga_path.name, str(output_path))
self.conversion_complete.emit(success, total) self.conversion_complete.emit(success, total)
@ -111,8 +113,9 @@ class TGAConverterDialog(QDialog):
layout.addWidget(header) layout.addWidget(header)
desc = QLabel( desc = QLabel(
"Convert Entropia Universe cached .tga icon files to PNG format.\n" "Convert Entropia Universe cached .tga icon files to PNG or WebP format.\n"
"The game stores item icons in the cache folder as .tga files." "The game stores item icons in the cache folder as .tga files.\n"
"WebP format is recommended for web uploads (smaller file size, better quality)."
) )
desc.setStyleSheet("color: #888; padding: 5px;") desc.setStyleSheet("color: #888; padding: 5px;")
desc.setWordWrap(True) desc.setWordWrap(True)
@ -168,6 +171,16 @@ class TGAConverterDialog(QDialog):
output_browse.clicked.connect(self._browse_output_folder) output_browse.clicked.connect(self._browse_output_folder)
output_layout.addWidget(output_browse) output_layout.addWidget(output_browse)
output_layout.addSpacing(20)
# Format selector
output_layout.addWidget(QLabel("Format:"))
self.format_combo = QComboBox()
self.format_combo.addItem("PNG", "png")
self.format_combo.addItem("WebP (better for web)", "webp")
self.format_combo.currentIndexChanged.connect(self._on_format_changed)
output_layout.addWidget(self.format_combo)
layout.addWidget(output_group) layout.addWidget(output_group)
# Convert button # Convert button
@ -275,6 +288,14 @@ class TGAConverterDialog(QDialog):
self.converter.output_dir = Path(dir_path) self.converter.output_dir = Path(dir_path)
self.output_path_label.setText(dir_path) self.output_path_label.setText(dir_path)
def _on_format_changed(self):
"""Handle output format change."""
output_format = self.format_combo.currentData()
if output_format == 'webp':
self.convert_btn.setText("🚀 Convert All to WebP")
else:
self.convert_btn.setText("🚀 Convert All to PNG")
def _start_conversion(self): def _start_conversion(self):
"""Start TGA to PNG conversion.""" """Start TGA to PNG conversion."""
if self.convert_worker and self.convert_worker.isRunning(): if self.convert_worker and self.convert_worker.isRunning():
@ -286,9 +307,10 @@ class TGAConverterDialog(QDialog):
self.results_list.clear() self.results_list.clear()
self.converted_files = [] self.converted_files = []
# Start worker with the selected cache path # Start worker with the selected cache path and format
cache_path = self.converter._cache_path if self.converter._cache_path else None cache_path = self.converter._cache_path if self.converter._cache_path else None
self.convert_worker = TGAConvertWorker(self.converter, cache_path) output_format = self.format_combo.currentData()
self.convert_worker = TGAConvertWorker(self.converter, cache_path, output_format)
self.convert_worker.progress_update.connect(self._on_progress) self.convert_worker.progress_update.connect(self._on_progress)
self.convert_worker.file_converted.connect(self._on_file_converted) self.convert_worker.file_converted.connect(self._on_file_converted)
self.convert_worker.conversion_complete.connect(self._on_conversion_complete) self.convert_worker.conversion_complete.connect(self._on_conversion_complete)