| from PIL import (
|
| Image,
|
| ImageEnhance,
|
| ImageFilter,
|
| ImageOps
|
| )
|
|
|
| from pathlib import Path
|
| from typing import Callable, Dict, Any
|
|
|
| import traceback
|
| import os
|
|
|
|
|
| class ImageConverter:
|
|
|
| SUPPORTED_FORMATS = {
|
| ".png",
|
| ".jpg",
|
| ".jpeg",
|
| ".webp",
|
| ".bmp",
|
| ".tiff"
|
| }
|
|
|
| def __init__(self):
|
|
|
|
|
| Image.MAX_IMAGE_PIXELS = None
|
|
|
| def convert(
|
| self,
|
| input_path: str,
|
| output_path: str,
|
| options: Dict[str, Any] | None = None,
|
| progress_callback: Callable = None
|
| ) -> bool:
|
|
|
| options = options or {}
|
|
|
| try:
|
|
|
| self._update_progress(
|
| progress_callback,
|
| 10
|
| )
|
|
|
|
|
| if not os.path.exists(input_path):
|
|
|
| raise FileNotFoundError(
|
| input_path
|
| )
|
|
|
| input_ext = Path(input_path).suffix.lower()
|
| output_ext = Path(output_path).suffix.lower()
|
|
|
|
|
| if output_ext not in self.SUPPORTED_FORMATS:
|
|
|
| raise ValueError(
|
| f"Unsupported format: {output_ext}"
|
| )
|
|
|
|
|
| img = Image.open(input_path)
|
|
|
|
|
| img = ImageOps.exif_transpose(img)
|
|
|
| self._update_progress(
|
| progress_callback,
|
| 25
|
| )
|
|
|
|
|
| if img.mode == "P":
|
| img = img.convert("RGBA")
|
|
|
|
|
| max_size = options.get("max_size")
|
|
|
| if max_size:
|
|
|
| img.thumbnail(
|
| (max_size, max_size)
|
| )
|
|
|
| self._update_progress(
|
| progress_callback,
|
| 40
|
| )
|
|
|
|
|
| if options.get(
|
| "ai_enhancement",
|
| False
|
| ):
|
|
|
| img = self.enhance_image(img)
|
|
|
| self._update_progress(
|
| progress_callback,
|
| 60
|
| )
|
|
|
|
|
| quality_settings = options.get(
|
| "quality_settings",
|
| {}
|
| )
|
|
|
| quality = int(
|
| quality_settings.get(
|
| "image",
|
| 85
|
| )
|
| )
|
|
|
|
|
| Path(output_path).parent.mkdir(
|
| parents=True,
|
| exist_ok=True
|
| )
|
|
|
|
|
| if output_ext in [".jpg", ".jpeg"]:
|
|
|
|
|
| if img.mode in ("RGBA", "LA"):
|
|
|
| background = Image.new(
|
| "RGB",
|
| img.size,
|
| (255, 255, 255)
|
| )
|
|
|
| background.paste(
|
| img,
|
| mask=img.split()[-1]
|
| )
|
|
|
| img = background
|
|
|
| else:
|
|
|
| img = img.convert("RGB")
|
|
|
| img.save(
|
| output_path,
|
| "JPEG",
|
| quality=quality,
|
| optimize=True
|
| )
|
|
|
| elif output_ext == ".png":
|
|
|
| compress_level = 9
|
|
|
| if quality >= 90:
|
| compress_level = 2
|
|
|
| elif quality >= 75:
|
| compress_level = 5
|
|
|
| img.save(
|
| output_path,
|
| "PNG",
|
| optimize=True,
|
| compress_level=compress_level
|
| )
|
|
|
| elif output_ext == ".webp":
|
|
|
| img.save(
|
| output_path,
|
| "WEBP",
|
| quality=quality,
|
| method=6
|
| )
|
|
|
| elif output_ext == ".bmp":
|
|
|
| if img.mode == "RGBA":
|
| img = img.convert("RGB")
|
|
|
| img.save(
|
| output_path,
|
| "BMP"
|
| )
|
|
|
| elif output_ext == ".tiff":
|
|
|
| img.save(
|
| output_path,
|
| "TIFF",
|
| compression="tiff_lzw"
|
| )
|
|
|
| else:
|
|
|
| img.save(output_path)
|
|
|
| self._update_progress(
|
| progress_callback,
|
| 100
|
| )
|
|
|
| print(
|
| f"✓ Converted: "
|
| f"{os.path.basename(input_path)} "
|
| f"→ {output_ext}"
|
| )
|
|
|
| return True
|
|
|
| except Exception as e:
|
|
|
| print(
|
| f"Image conversion error: {e}"
|
| )
|
|
|
| traceback.print_exc()
|
|
|
| return False
|
|
|
| def _update_progress(
|
| self,
|
| callback,
|
| value
|
| ):
|
|
|
| try:
|
|
|
| if callback:
|
| callback(value)
|
|
|
| except Exception as e:
|
|
|
| print(
|
| f"Progress callback error: {e}"
|
| )
|
|
|
| def enhance_image(
|
| self,
|
| img
|
| ):
|
|
|
| try:
|
|
|
|
|
| enhancer = ImageEnhance.Contrast(img)
|
|
|
| img = enhancer.enhance(1.08)
|
|
|
|
|
| enhancer = ImageEnhance.Sharpness(img)
|
|
|
| img = enhancer.enhance(1.1)
|
|
|
|
|
| enhancer = ImageEnhance.Color(img)
|
|
|
| img = enhancer.enhance(1.03)
|
|
|
|
|
| img = img.filter(
|
| ImageFilter.SHARPEN
|
| )
|
|
|
| return img
|
|
|
| except Exception as e:
|
|
|
| print(
|
| f"Enhancement error: {e}"
|
| )
|
|
|
| return img |