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:
parent
ac334ac4a8
commit
b666e40bfc
|
|
@ -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,12 +300,7 @@ 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}.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"
|
output_path = self.output_dir / f"{output_name}.png"
|
||||||
image.save(output_path, 'PNG')
|
image.save(output_path, 'PNG')
|
||||||
|
|
||||||
|
|
@ -334,11 +331,7 @@ 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}.webp"
|
|
||||||
image.save(output_path, 'WEBP', quality=95, method=6)
|
|
||||||
else:
|
|
||||||
output_path = self.output_dir / f"{output_name}.png"
|
output_path = self.output_dir / f"{output_name}.png"
|
||||||
image.save(output_path, 'PNG')
|
image.save(output_path, 'PNG')
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue