|
""" |
|
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 |
|
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) -> Dict: |
|
""" |
|
Get optimal save settings for maximum quality preservation. |
|
|
|
Args: |
|
format_name: PIL format name (e.g., 'JPEG', 'PNG') |
|
|
|
Returns: |
|
Dictionary of save settings for the format |
|
""" |
|
settings = {} |
|
|
|
if format_name == 'JPEG': |
|
settings = { |
|
'quality': 100, |
|
'optimize': False, |
|
'progressive': False, |
|
'subsampling': 0 |
|
} |
|
|
|
elif format_name == 'PNG': |
|
settings = { |
|
'optimize': False, |
|
'compress_level': 1 |
|
} |
|
|
|
elif format_name == 'WEBP': |
|
settings = { |
|
'lossless': True, |
|
'quality': 100, |
|
'method': 6 |
|
} |
|
|
|
elif format_name == 'TIFF': |
|
settings = { |
|
'compression': None |
|
} |
|
|
|
elif format_name == 'BMP': |
|
settings = {} |
|
|
|
elif format_name == 'GIF': |
|
settings = { |
|
'optimize': False |
|
} |
|
|
|
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 |
|
|
|
|
|
if 'dpi' in original_image.info and format_name in ['JPEG', 'PNG', 'TIFF', 'WEBP']: |
|
metadata['dpi'] = original_image.info['dpi'] |
|
|
|
|
|
if 'icc_profile' in original_image.info and format_name in ['JPEG', 'PNG', 'TIFF', 'WEBP']: |
|
metadata['icc_profile'] = original_image.info['icc_profile'] |
|
|
|
|
|
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 |
|
""" |
|
|
|
converted_image = image.copy() |
|
|
|
|
|
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': |
|
|
|
converted_image = converted_image.convert('RGBA') |
|
|
|
|
|
background = Image.new("RGB", converted_image.size, (255, 255, 255)) |
|
if converted_image.mode in ('RGBA', 'LA'): |
|
|
|
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 _verify_conversion_quality(self, original_size: Tuple[int, int], output_path: str) -> 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 |
|
|
|
Returns: |
|
True if verification passes |
|
|
|
Raises: |
|
Exception: If verification fails |
|
""" |
|
if not os.path.exists(output_path): |
|
raise Exception("Output file was not created") |
|
|
|
|
|
file_size = os.path.getsize(output_path) |
|
logger_config.info(f"β Output file created: {file_size:,} bytes") |
|
|
|
|
|
with Image.open(output_path) as verify_img: |
|
if verify_img.size != original_size: |
|
raise Exception(f"Dimensions changed! Expected: {original_size}, Got: {verify_img.size}") |
|
|
|
logger_config.info(f"β Aspect ratio preserved: {verify_img.size}") |
|
|
|
return True |
|
|
|
def convert_image(self, input_file_name: str, output_format: str) -> 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 |
|
|
|
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: |
|
|
|
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}") |
|
|
|
|
|
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}") |
|
|
|
|
|
converted_image = self._convert_color_mode(original_image, target_format) |
|
|
|
|
|
if converted_image.size != original_size: |
|
raise Exception(f"Size changed during conversion! Original: {original_size}, Current: {converted_image.size}") |
|
|
|
|
|
output_path = self._generate_output_path(output_format) |
|
|
|
|
|
save_settings = self._get_optimal_save_settings(target_format) |
|
|
|
|
|
metadata = self._preserve_metadata(original_image, target_format) |
|
save_settings.update(metadata) |
|
|
|
logger_config.info(f"Converting to {target_format} with maximum quality") |
|
|
|
|
|
converted_image.save(output_path, format=target_format, **save_settings) |
|
|
|
|
|
self._verify_conversion_quality(original_size, output_path) |
|
|
|
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 |
|
|
|
|
|
if __name__ == "__main__": |
|
converter = Converter() |
|
converter.convert_image("image/test.HEIC", "png") |