From b666e40bfc3df405f4482c15550f1ee28fd4d302 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Wed, 11 Feb 2026 16:37:14 +0000 Subject: [PATCH] feat: add canvas sizing with centered icons and upscale option - Removed WebP support (PNG only now) - Added canvas size selector: Original, 64x64, 128x128, 256x256, 280x280, 320x320, 512x512 - Icons are centered on transparent canvas - Added 'Upscale small icons' checkbox to scale up small icons to better fill canvas - Uses high-quality Lanczos resampling for upscaling - Default canvas size: 320x320 --- modules/tga_converter.py | 93 +++++++++++++++++++++++++++----------- ui/tga_converter_dialog.py | 71 ++++++++++++++++++----------- 2 files changed, 110 insertions(+), 54 deletions(-) diff --git a/modules/tga_converter.py b/modules/tga_converter.py index aff81c9..82e59bc 100644 --- a/modules/tga_converter.py +++ b/modules/tga_converter.py @@ -264,17 +264,19 @@ class TGAConverter: return None def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None, - output_format: str = 'png') -> Optional[Path]: + canvas_size: Optional[Tuple[int, int]] = None, + upscale: bool = False) -> Optional[Path]: """ - Convert a TGA file to PNG or WebP. + Convert a TGA file to PNG with optional canvas sizing. Args: tga_path: Path to .tga file output_name: Optional output filename (without extension) - output_format: 'png' or 'webp' + canvas_size: Optional (width, height) for output canvas + upscale: Whether to upscale small icons to fit canvas better Returns: - Path to converted file or None if failed + Path to converted PNG file or None if failed """ if not PIL_AVAILABLE: logger.error("PIL/Pillow required for TGA to PNG conversion") @@ -298,14 +300,9 @@ class TGAConverter: if output_name is None: output_name = tga_path.stem - # 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') + # Save as PNG + output_path = self.output_dir / f"{output_name}.png" + image.save(output_path, 'PNG') logger.info(f"Converted (PIL): {tga_path.name} -> {output_path.name}") return output_path @@ -334,13 +331,9 @@ class TGAConverter: if output_name is None: output_name = tga_path.stem - # 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') + # Save as PNG + output_path = self.output_dir / f"{output_name}.png" + image.save(output_path, 'PNG') logger.info(f"Converted (manual): {tga_path.name} -> {output_path.name}") return output_path @@ -350,16 +343,18 @@ class TGAConverter: return None def convert_cache_folder(self, cache_path: Optional[Path] = None, - output_format: str = 'png') -> Dict[str, Path]: + canvas_size: Optional[Tuple[int, int]] = None, + upscale: bool = False) -> Dict[str, Path]: """ - Convert all TGA files in cache folder to PNG or WebP. + Convert all TGA files in cache folder to PNG. Args: cache_path: Path to cache folder (auto-detect if None) - output_format: 'png' or 'webp' + canvas_size: Optional (width, height) for output canvas + upscale: Whether to upscale small icons Returns: - Dictionary mapping TGA filenames to output paths + Dictionary mapping TGA filenames to PNG paths """ results = {} @@ -377,20 +372,64 @@ 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 to {output_format.upper()}") + 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}") - output_path = self.convert_tga_to_png(tga_path, output_format=output_format) - if output_path: - results[tga_path.name] = output_path + png_path = self.convert_tga_to_png(tga_path, canvas_size=canvas_size, upscale=upscale) + 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 _apply_canvas(self, image, canvas_size, upscale=False): + """ + Place image centered on a canvas of specified size. + + Args: + image: Source PIL Image + canvas_size: (width, height) for output canvas + upscale: Whether to upscale small images to fit canvas better + + Returns: + New image with canvas size, source image centered + """ + from PIL import Image + + canvas_w, canvas_h = canvas_size + img_w, img_h = image.size + + # Create transparent canvas + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + + # Calculate scaling + if upscale: + # Scale up to fit within canvas with some padding (e.g., 90%) + max_size = int(min(canvas_w, canvas_h) * 0.9) + scale = min(max_size / img_w, max_size / img_h) + + if scale > 1: # Only upscale, never downscale + new_w = int(img_w * scale) + new_h = int(img_h * scale) + image = image.resize((new_w, new_h), Image.Resampling.LANCZOS) + img_w, img_h = new_w, new_h + + # Calculate centered position + x = (canvas_w - img_w) // 2 + y = (canvas_h - img_h) // 2 + + # Paste image onto canvas + if image.mode == 'RGBA': + canvas.paste(image, (x, y), image) + else: + canvas.paste(image, (x, y)) + + return canvas + def get_conversion_summary(self) -> str: """Get a summary of available TGA files and conversion status.""" cache_path = self.find_cache_folder() diff --git a/ui/tga_converter_dialog.py b/ui/tga_converter_dialog.py index 62be5bc..17450b9 100644 --- a/ui/tga_converter_dialog.py +++ b/ui/tga_converter_dialog.py @@ -30,11 +30,12 @@ class TGAConvertWorker(QThread): conversion_error = pyqtSignal(str) def __init__(self, converter: TGAConverter, cache_path: Optional[Path] = None, - output_format: str = 'png'): + canvas_size=None, upscale: bool = False): super().__init__() self.converter = converter self.cache_path = cache_path - self.output_format = output_format + self.canvas_size = canvas_size + self.upscale = upscale self._is_running = True def run(self): @@ -57,7 +58,8 @@ class TGAConvertWorker(QThread): total = len(tga_files) success = 0 - self.progress_update.emit(f"Found {total} TGA files to convert to {self.output_format.upper()}") + 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: @@ -65,7 +67,11 @@ class TGAConvertWorker(QThread): self.progress_update.emit(f"[{i+1}/{total}] Converting: {tga_path.name}") - output_path = self.converter.convert_tga_to_png(tga_path, output_format=self.output_format) + output_path = self.converter.convert_tga_to_png( + tga_path, + canvas_size=self.canvas_size, + upscale=self.upscale + ) if output_path: success += 1 self.file_converted.emit(tga_path.name, str(output_path)) @@ -113,9 +119,9 @@ class TGAConverterDialog(QDialog): layout.addWidget(header) desc = QLabel( - "Convert Entropia Universe cached .tga icon files to PNG or WebP format.\n" + "Convert Entropia Universe cached .tga icon files to PNG 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)." + "Icons are centered on a canvas of your chosen size with transparent background." ) desc.setStyleSheet("color: #888; padding: 5px;") desc.setWordWrap(True) @@ -171,18 +177,34 @@ 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) + # 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(4) # Default to 320x320 + canvas_layout.addWidget(self.canvas_combo) + + canvas_layout.addSpacing(20) + + self.upscale_check = QCheckBox("Upscale small icons") + self.upscale_check.setChecked(True) + self.upscale_check.setToolTip("Scale up small icons to better fill the canvas") + canvas_layout.addWidget(self.upscale_check) + + canvas_layout.addStretch() + layout.addWidget(canvas_group) + # Convert button self.convert_btn = QPushButton("🚀 Convert All to PNG") self.convert_btn.setMinimumHeight(50) @@ -288,14 +310,6 @@ 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(): @@ -307,10 +321,13 @@ class TGAConverterDialog(QDialog): self.results_list.clear() self.converted_files = [] - # Start worker with the selected cache path and format + # Get canvas settings + canvas_size = self.canvas_combo.currentData() + upscale = self.upscale_check.isChecked() + + # Start worker with settings cache_path = self.converter._cache_path if self.converter._cache_path else None - output_format = self.format_combo.currentData() - self.convert_worker = TGAConvertWorker(self.converter, cache_path, output_format) + self.convert_worker = TGAConvertWorker(self.converter, cache_path, canvas_size, upscale) 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)