From 0034cc9453959d26998d97a8c57170888070d548 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Wed, 11 Feb 2026 17:25:00 +0000 Subject: [PATCH] feat: add 3 upscale methods - Sharp Pixels, Smooth HQ4x-style, Photorealistic - NEAREST: Sharp pixel edges (best for clean pixel art) - HQ4x-style: Integer scale first, then smooth to target with edge enhancement - LANCZOS: Very smooth (best for photorealistic textures) - Added upscale method selector to UI - Each method uses UnsharpMask for enhanced crispness --- modules/tga_converter.py | 44 +++++++++++++++++++++++++++----------- ui/tga_converter_dialog.py | 28 +++++++++++++++++------- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/modules/tga_converter.py b/modules/tga_converter.py index 75e1482..26841d2 100644 --- a/modules/tga_converter.py +++ b/modules/tga_converter.py @@ -279,7 +279,7 @@ class TGAConverter: def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None, canvas_size: Optional[Tuple[int, int]] = None, - upscale: bool = False) -> Optional[Path]: + upscale: bool = False, upscale_method: str = 'nearest') -> Optional[Path]: """ Convert a TGA file to PNG with optional canvas sizing. @@ -312,7 +312,7 @@ class TGAConverter: # Apply canvas sizing if requested if canvas_size: - image = self._apply_canvas(image, canvas_size, upscale) + image = self._apply_canvas(image, canvas_size, upscale, upscale_method) # Determine output path and format if output_name is None: @@ -347,7 +347,7 @@ class TGAConverter: # Apply canvas sizing if requested if canvas_size: - image = self._apply_canvas(image, canvas_size, upscale) + image = self._apply_canvas(image, canvas_size, upscale, upscale_method) # Determine output path and format if output_name is None: @@ -408,7 +408,7 @@ class TGAConverter: return results - def _apply_canvas(self, image, canvas_size, upscale=False): + def _apply_canvas(self, image, canvas_size, upscale=False, upscale_method='nearest'): """ Place image centered on a canvas of specified size. @@ -416,6 +416,7 @@ class TGAConverter: image: Source PIL Image canvas_size: (width, height) for output canvas upscale: Whether to upscale small images to fit canvas better + upscale_method: 'nearest' (sharp pixels), 'hq4x' (smoothed), or 'lanczos' (smooth) Returns: New image with canvas size, source image centered @@ -435,17 +436,34 @@ class TGAConverter: scale = min(max_size / img_w, max_size / img_h) if scale > 1: # Only upscale, never downscale - # For pixel art/game icons, use integer scaling + NEAREST for sharpness - # Round scale to nearest integer for clean pixel edges - int_scale = max(1, round(scale)) - new_w = img_w * int_scale - new_h = img_h * int_scale + new_w = int(img_w * scale) + new_h = int(img_h * scale) - # Use NEAREST neighbor for pixel art - keeps edges sharp - image = image.resize((new_w, new_h), Image.Resampling.NEAREST) + if upscale_method == 'nearest': + # NEAREST: Sharp pixels, best for clean pixel art + image = image.resize((new_w, new_h), Image.Resampling.NEAREST) + + elif upscale_method == 'hq4x': + # HQ4x-style: Integer scale first, then smooth to target + int_scale = max(2, int(scale)) + temp_w = img_w * int_scale + temp_h = img_h * int_scale + + # Scale up with NEAREST (integer multiple) + image = image.resize((temp_w, temp_h), Image.Resampling.NEAREST) + + # Then downscale to target with LANCZOS for smoothing + image = image.resize((new_w, new_h), Image.Resampling.LANCZOS) + + # Enhance edges + image = image.filter(ImageFilter.EDGE_ENHANCE_MORE) + + else: # lanczos + # LANCZOS: Smooth, best for photorealistic textures + image = image.resize((new_w, new_h), Image.Resampling.LANCZOS) - # Apply slight sharpening to reduce blur from the upscale - image = image.filter(ImageFilter.SHARPEN) + # Apply subtle sharpening for crispness + image = image.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3)) img_w, img_h = new_w, new_h diff --git a/ui/tga_converter_dialog.py b/ui/tga_converter_dialog.py index 17450b9..064282e 100644 --- a/ui/tga_converter_dialog.py +++ b/ui/tga_converter_dialog.py @@ -30,12 +30,13 @@ class TGAConvertWorker(QThread): conversion_error = pyqtSignal(str) def __init__(self, converter: TGAConverter, cache_path: Optional[Path] = None, - canvas_size=None, upscale: bool = False): + canvas_size=None, upscale: bool = False, upscale_method: str = 'nearest'): super().__init__() self.converter = converter self.cache_path = cache_path self.canvas_size = canvas_size self.upscale = upscale + self.upscale_method = upscale_method self._is_running = True def run(self): @@ -70,7 +71,8 @@ class TGAConvertWorker(QThread): output_path = self.converter.convert_tga_to_png( tga_path, canvas_size=self.canvas_size, - upscale=self.upscale + upscale=self.upscale, + upscale_method=self.upscale_method ) if output_path: success += 1 @@ -197,10 +199,17 @@ class TGAConverterDialog(QDialog): 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.addWidget(QLabel("Upscale:")) + self.upscale_method_combo = QComboBox() + self.upscale_method_combo.addItem("Sharp Pixels (NEAREST)", "nearest") + self.upscale_method_combo.addItem("Smooth (HQ4x-style)", "hq4x") + self.upscale_method_combo.addItem("Photorealistic (LANCZOS)", "lanczos") + self.upscale_method_combo.setToolTip( + "NEAREST: Sharp pixel edges (best for pixel art)\n" + "HQ4x: Smooth but keeps details (best for game icons)\n" + "LANCZOS: Very smooth (best for photos)" + ) + canvas_layout.addWidget(self.upscale_method_combo) canvas_layout.addStretch() layout.addWidget(canvas_group) @@ -323,11 +332,14 @@ class TGAConverterDialog(QDialog): # Get canvas settings canvas_size = self.canvas_combo.currentData() - upscale = self.upscale_check.isChecked() + upscale_method = self.upscale_method_combo.currentData() # Start worker with settings cache_path = self.converter._cache_path if self.converter._cache_path else None - self.convert_worker = TGAConvertWorker(self.converter, cache_path, canvas_size, upscale) + self.convert_worker = TGAConvertWorker( + self.converter, cache_path, canvas_size, + upscale=True, upscale_method=upscale_method + ) 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)