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
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,12 +300,7 @@ 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:
# Save as PNG
output_path = self.output_dir / f"{output_name}.png"
image.save(output_path, 'PNG')
@ -334,11 +331,7 @@ 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:
# Save as PNG
output_path = self.output_dir / f"{output_name}.png"
image.save(output_path, 'PNG')
@ -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()

View File

@ -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)