From ac334ac4a842844743f95493d20a4d686a140810 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Wed, 11 Feb 2026 16:17:31 +0000 Subject: [PATCH] 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 --- modules/tga_converter.py | 55 ++++++++++++++++++++++++-------------- ui/tga_converter_dialog.py | 44 ++++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/modules/tga_converter.py b/modules/tga_converter.py index bde868d..aff81c9 100644 --- a/modules/tga_converter.py +++ b/modules/tga_converter.py @@ -263,16 +263,18 @@ class TGAConverter: 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]: + 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: tga_path: Path to .tga file output_name: Optional output filename (without extension) + output_format: 'png' or 'webp' Returns: - Path to converted PNG file or None if failed + Path to converted file or None if failed """ if not PIL_AVAILABLE: logger.error("PIL/Pillow required for TGA to PNG conversion") @@ -292,15 +294,21 @@ class TGAConverter: else: image = image.convert('RGBA') - # Determine output path + # Determine output path and format if output_name is None: output_name = tga_path.stem - png_path = self.output_dir / f"{output_name}.png" - image.save(png_path, 'PNG') + # Save as PNG or WebP + 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}") - return png_path + logger.info(f"Converted (PIL): {tga_path.name} -> {output_path.name}") + return output_path except Exception as pil_error: logger.debug(f"PIL failed, trying manual: {pil_error}") @@ -322,29 +330,36 @@ class TGAConverter: else: image = Image.fromarray(pixels, 'RGB') - # Determine output path + # Determine output path and format if output_name is None: output_name = tga_path.stem - png_path = self.output_dir / f"{output_name}.png" - image.save(png_path, 'PNG') + # Save as PNG or WebP + 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}") - return png_path + logger.info(f"Converted (manual): {tga_path.name} -> {output_path.name}") + return output_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]: + 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: cache_path: Path to cache folder (auto-detect if None) + output_format: 'png' or 'webp' Returns: - Dictionary mapping TGA filenames to PNG paths + Dictionary mapping TGA filenames to output paths """ results = {} @@ -362,14 +377,14 @@ class TGAConverter: logger.warning(f"No .tga files found in: {cache_path}") 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 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 + output_path = self.convert_tga_to_png(tga_path, output_format=output_format) + if output_path: + results[tga_path.name] = output_path logger.info(f"Converted {len(results)}/{len(tga_files)} files successfully") logger.info(f"Output directory: {self.output_dir}") diff --git a/ui/tga_converter_dialog.py b/ui/tga_converter_dialog.py index 9265fd7..62be5bc 100644 --- a/ui/tga_converter_dialog.py +++ b/ui/tga_converter_dialog.py @@ -11,7 +11,7 @@ from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QListWidget, QListWidgetItem, QGroupBox, QProgressBar, QTextEdit, QFileDialog, - QMessageBox, QSpinBox, QCheckBox + QMessageBox, QSpinBox, QCheckBox, QComboBox ) from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QPixmap @@ -25,14 +25,16 @@ class TGAConvertWorker(QThread): """Background worker for TGA conversion.""" 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_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__() self.converter = converter self.cache_path = cache_path + self.output_format = output_format self._is_running = True def run(self): @@ -55,7 +57,7 @@ class TGAConvertWorker(QThread): total = len(tga_files) 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): if not self._is_running: @@ -63,10 +65,10 @@ class TGAConvertWorker(QThread): 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: + output_path = self.converter.convert_tga_to_png(tga_path, output_format=self.output_format) + if output_path: 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) @@ -111,8 +113,9 @@ class TGAConverterDialog(QDialog): 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." + "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.\n" + "WebP format is recommended for web uploads (smaller file size, better quality)." ) desc.setStyleSheet("color: #888; padding: 5px;") desc.setWordWrap(True) @@ -168,6 +171,16 @@ class TGAConverterDialog(QDialog): output_browse.clicked.connect(self._browse_output_folder) 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) # Convert button @@ -275,6 +288,14 @@ class TGAConverterDialog(QDialog): self.converter.output_dir = Path(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): """Start TGA to PNG conversion.""" if self.convert_worker and self.convert_worker.isRunning(): @@ -286,9 +307,10 @@ class TGAConverterDialog(QDialog): self.results_list.clear() 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 - 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.file_converted.connect(self._on_file_converted) self.convert_worker.conversion_complete.connect(self._on_conversion_complete)