""" High-Quality Image Converter Module This module provides a class-based image converter that preserves original quality and aspect ratio during format conversion. Supports all major image formats. """ from .image_base import ImageBase from PIL import Image import os import base64 import io from typing import Dict, Optional, Tuple from custom_logger import logger_config import pillow_heif pillow_heif.register_heif_opener() class Converter(ImageBase): """ High-quality image format converter that preserves original quality and aspect ratio. This class handles conversion between various image formats while maintaining the highest possible quality and exact dimensional preservation. """ def __init__(self): super().__init__("converter") def _validate_output_format(self, output_format: str) -> str: """ Validate and normalize the output format. Args: output_format: Desired output format Returns: Normalized PIL format name Raises: ValueError: If format is not supported """ if not output_format or not output_format.strip(): raise ValueError("Output format cannot be empty") format_clean = output_format.lower().strip() if format_clean not in self.supported_formats: supported_list = ', '.join(self.supported_formats.keys()) raise ValueError(f"Unsupported format '{output_format}'. Supported: {supported_list}") return self.supported_formats[format_clean] def _get_optimal_save_settings(self, format_name: str, quality: int = 100) -> Dict: """ Get optimal save settings for maximum quality preservation. Args: format_name: PIL format name (e.g., 'JPEG', 'PNG') quality: Quality setting (1-100) Returns: Dictionary of save settings for the format """ settings = {} if format_name == 'JPEG': settings = { 'quality': quality, 'optimize': True, 'progressive': True, 'subsampling': 0 } elif format_name == 'PNG': settings = { 'optimize': True, 'compress_level': 9 if quality < 100 else 1 } elif format_name == 'WEBP': settings = { 'lossless': quality == 100, 'quality': quality, 'method': 6 } elif format_name == 'TIFF': settings = { 'compression': 'tiff_lzw' if quality < 100 else None } elif format_name == 'BMP': settings = {} # BMP is always lossless elif format_name == 'GIF': settings = { 'optimize': True } elif format_name == 'HEIC': settings = { 'quality': quality } return settings def _preserve_metadata(self, original_image: Image.Image, format_name: str) -> Dict: """ Extract metadata from original image that's compatible with target format. Args: original_image: Original PIL Image object format_name: Target PIL format name Returns: Dictionary of metadata to preserve """ metadata = {} if not hasattr(original_image, 'info') or not original_image.info: return metadata # DPI preservation if 'dpi' in original_image.info and format_name in ['JPEG', 'PNG', 'TIFF', 'WEBP']: metadata['dpi'] = original_image.info['dpi'] # Color profile preservation if 'icc_profile' in original_image.info and format_name in ['JPEG', 'PNG', 'TIFF', 'WEBP']: metadata['icc_profile'] = original_image.info['icc_profile'] # EXIF data preservation if 'exif' in original_image.info and format_name in ['JPEG', 'TIFF', 'WEBP']: metadata['exif'] = original_image.info['exif'] return metadata def _convert_color_mode(self, image: Image.Image, target_format: str) -> Image.Image: """ Convert image color mode if required by target format, preserving quality. Args: image: PIL Image object target_format: Target PIL format name Returns: Image with appropriate color mode """ # Create exact copy to preserve original converted_image = image.copy() # Handle formats that don't support transparency if target_format == 'JPEG' and converted_image.mode in ('RGBA', 'LA', 'P'): logger_config.info("Converting to RGB (JPEG doesn't support transparency)") if converted_image.mode == 'P': # Preserve palette colors during conversion converted_image = converted_image.convert('RGBA') # Create white background and blend with perfect quality background = Image.new("RGB", converted_image.size, (255, 255, 255)) if converted_image.mode in ('RGBA', 'LA'): # Use alpha_composite for perfect quality preservation bg_rgba = background.convert('RGBA') composite = Image.alpha_composite(bg_rgba, converted_image.convert('RGBA')) converted_image = composite.convert('RGB') elif target_format == 'BMP' and converted_image.mode in ('RGBA', 'LA'): logger_config.info("Converting to RGB (BMP doesn't support transparency)") background = Image.new("RGB", converted_image.size, (255, 255, 255)) bg_rgba = background.convert('RGBA') composite = Image.alpha_composite(bg_rgba, converted_image.convert('RGBA')) converted_image = composite.convert('RGB') return converted_image def _convert_to_svg(self, image: Image.Image, output_path: str, original_size: Tuple[int, int]) -> None: """ Convert a raster image to SVG by embedding it as a base64-encoded image. Args: image: PIL Image object output_path: Path to save the SVG file original_size: Original image dimensions (width, height) """ # Convert image to PNG bytes for embedding buffer = io.BytesIO() # Ensure we save in a format that preserves transparency if image.mode in ('RGBA', 'LA', 'P'): image.save(buffer, format='PNG', optimize=False) mime_type = 'image/png' else: image.save(buffer, format='PNG', optimize=False) mime_type = 'image/png' buffer.seek(0) image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') width, height = original_size # Create SVG with embedded image svg_content = f''' ''' with open(output_path, 'w', encoding='utf-8') as f: f.write(svg_content) # Verify file was created if not os.path.exists(output_path): raise Exception("SVG file was not created") file_size = os.path.getsize(output_path) logger_config.info(f"✓ SVG file created: {file_size:,} bytes") def _verify_conversion_quality(self, original_size: Tuple[int, int], output_path: str, scale: float = 1.0) -> bool: """ Verify that the converted image maintains original quality and dimensions. Args: original_size: Original image dimensions (width, height) output_path: Path to converted image scale: Scale factor used Returns: True if verification passes Raises: Exception: If verification fails """ if not os.path.exists(output_path): raise Exception("Output file was not created") # Check file size file_size = os.path.getsize(output_path) logger_config.info(f"✓ Output file created: {file_size:,} bytes") # Calculate expected size expected_size = (int(original_size[0] * scale), int(original_size[1] * scale)) # Verify dimensions are preserved with Image.open(output_path) as verify_img: # Allow for slight rounding differences (±1 pixel) if abs(verify_img.size[0] - expected_size[0]) > 1 or abs(verify_img.size[1] - expected_size[1]) > 1: raise Exception(f"Dimensions changed! Expected: {expected_size}, Got: {verify_img.size}") logger_config.info(f"✓ Dimensions verified: {verify_img.size}") return True def convert_image(self, input_file_name: str, output_format: str, quality: int = 100, scale: float = 1.0) -> str: """ Convert an image to the specified format with maximum quality and ratio preservation. Args: input_file: Path to the input image file output_format: Target image format quality: Quality setting (1-100) scale: Scale factor (0.1-1.0) Returns: Path to the converted output file Raises: FileNotFoundError: If input file doesn't exist ValueError: If parameters are invalid Exception: If conversion fails """ try: # Validate inputs self.input_file_name = input_file_name self.input_file_path = f'{self.input_dir}/{self.input_file_name}' self._validate_input_file() target_format = self._validate_output_format(output_format) logger_config.info(f"Starting conversion: {self.input_file_path} (Quality: {quality}, Scale: {scale})") # Open and analyze original image with Image.open(self.input_file_path) as original_image: original_size = original_image.size original_mode = original_image.mode original_format = original_image.format logger_config.info(f"Original - Format: {original_format}, Mode: {original_mode}, Size: {original_size}") # Convert color mode if necessary converted_image = self._convert_color_mode(original_image, target_format) # Apply scaling if needed if scale < 1.0: new_size = (int(original_size[0] * scale), int(original_size[1] * scale)) logger_config.info(f"Resizing image to {new_size}") converted_image = converted_image.resize(new_size, Image.Resampling.LANCZOS) # Generate output path output_path = self._generate_output_path(output_format) # Handle SVG conversion specially if target_format == 'SVG': logger_config.info(f"Converting to SVG (embedding as base64)") self._convert_to_svg(converted_image, output_path, converted_image.size) else: # Get optimal save settings save_settings = self._get_optimal_save_settings(target_format, quality) # Preserve metadata metadata = self._preserve_metadata(original_image, target_format) save_settings.update(metadata) logger_config.info(f"Converting to {target_format} with quality {quality}") # Perform conversion with quality preservation converted_image.save(output_path, format=target_format, **save_settings) # Verify conversion quality self._verify_conversion_quality(original_size, output_path, scale) logger_config.info(f"✓ Conversion completed successfully: {output_path}") return output_path except Exception as e: logger_config.info(f"Image conversion failed: {str(e)}") return None # Example usage if __name__ == "__main__": converter = Converter() converter.convert_image("image/test.HEIC", "png", quality=80, scale=0.5)