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
This commit is contained in:
LemonNexus 2026-02-11 16:37:14 +00:00
parent ac334ac4a8
commit b666e40bfc
2 changed files with 110 additions and 54 deletions

View File

@ -264,17 +264,19 @@ class TGAConverter:
return None return None
def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = 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: 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' canvas_size: Optional (width, height) for output canvas
upscale: Whether to upscale small icons to fit canvas better
Returns: Returns:
Path to converted file or None if failed Path to converted PNG 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")
@ -298,14 +300,9 @@ class TGAConverter:
if output_name is None: if output_name is None:
output_name = tga_path.stem output_name = tga_path.stem
# Save as PNG or WebP # Save as PNG
if output_format.lower() == 'webp': output_path = self.output_dir / f"{output_name}.png"
output_path = self.output_dir / f"{output_name}.webp" image.save(output_path, 'PNG')
# 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} -> {output_path.name}") logger.info(f"Converted (PIL): {tga_path.name} -> {output_path.name}")
return output_path return output_path
@ -334,13 +331,9 @@ class TGAConverter:
if output_name is None: if output_name is None:
output_name = tga_path.stem output_name = tga_path.stem
# Save as PNG or WebP # Save as PNG
if output_format.lower() == 'webp': output_path = self.output_dir / f"{output_name}.png"
output_path = self.output_dir / f"{output_name}.webp" image.save(output_path, 'PNG')
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} -> {output_path.name}") logger.info(f"Converted (manual): {tga_path.name} -> {output_path.name}")
return output_path return output_path
@ -350,16 +343,18 @@ class TGAConverter:
return None return None
def convert_cache_folder(self, cache_path: Optional[Path] = 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: 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' canvas_size: Optional (width, height) for output canvas
upscale: Whether to upscale small icons
Returns: Returns:
Dictionary mapping TGA filenames to output paths Dictionary mapping TGA filenames to PNG paths
""" """
results = {} results = {}
@ -377,20 +372,64 @@ 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 to {output_format.upper()}") logger.info(f"Found {len(tga_files)} TGA files to convert")
# 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}")
output_path = self.convert_tga_to_png(tga_path, output_format=output_format) png_path = self.convert_tga_to_png(tga_path, canvas_size=canvas_size, upscale=upscale)
if output_path: if png_path:
results[tga_path.name] = output_path results[tga_path.name] = png_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}")
return results 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: def get_conversion_summary(self) -> str:
"""Get a summary of available TGA files and conversion status.""" """Get a summary of available TGA files and conversion status."""
cache_path = self.find_cache_folder() cache_path = self.find_cache_folder()

View File

@ -30,11 +30,12 @@ class TGAConvertWorker(QThread):
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'): canvas_size=None, upscale: bool = False):
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.canvas_size = canvas_size
self.upscale = upscale
self._is_running = True self._is_running = True
def run(self): def run(self):
@ -57,7 +58,8 @@ class TGAConvertWorker(QThread):
total = len(tga_files) total = len(tga_files)
success = 0 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): for i, tga_path in enumerate(tga_files):
if not self._is_running: if not self._is_running:
@ -65,7 +67,11 @@ 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}")
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: if output_path:
success += 1 success += 1
self.file_converted.emit(tga_path.name, str(output_path)) self.file_converted.emit(tga_path.name, str(output_path))
@ -113,9 +119,9 @@ class TGAConverterDialog(QDialog):
layout.addWidget(header) layout.addWidget(header)
desc = QLabel( 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" "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.setStyleSheet("color: #888; padding: 5px;")
desc.setWordWrap(True) desc.setWordWrap(True)
@ -171,18 +177,34 @@ 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)
# 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 # Convert button
self.convert_btn = QPushButton("🚀 Convert All to PNG") self.convert_btn = QPushButton("🚀 Convert All to PNG")
self.convert_btn.setMinimumHeight(50) self.convert_btn.setMinimumHeight(50)
@ -288,14 +310,6 @@ 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():
@ -307,10 +321,13 @@ class TGAConverterDialog(QDialog):
self.results_list.clear() self.results_list.clear()
self.converted_files = [] 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 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, canvas_size, upscale)
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)