Tools / image /converter.py
jebin2's picture
image converter added
609d54c
"""
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, # Maximum quality
'optimize': False, # Don't optimize (can reduce quality)
'progressive': False, # Standard baseline JPEG
'subsampling': 0 # No chroma subsampling (4:4:4)
}
elif format_name == 'PNG':
settings = {
'optimize': False, # Don't optimize to preserve exact quality
'compress_level': 1 # Minimal compression for maximum quality
}
elif format_name == 'WEBP':
settings = {
'lossless': True, # Lossless compression
'quality': 100, # Maximum quality (for fallback)
'method': 6 # Best compression method
}
elif format_name == 'TIFF':
settings = {
'compression': None # No compression for perfect quality
}
elif format_name == 'BMP':
settings = {} # BMP is always lossless
elif format_name == 'GIF':
settings = {
'optimize': False # Don't optimize palette
}
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 _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")
# Check file size
file_size = os.path.getsize(output_path)
logger_config.info(f"βœ“ Output file created: {file_size:,} bytes")
# Verify dimensions are preserved
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:
# 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}")
# 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)
# Verify size preservation (safety check)
if converted_image.size != original_size:
raise Exception(f"Size changed during conversion! Original: {original_size}, Current: {converted_image.size}")
# Generate output path
output_path = self._generate_output_path(output_format)
# Get optimal save settings
save_settings = self._get_optimal_save_settings(target_format)
# Preserve metadata
metadata = self._preserve_metadata(original_image, target_format)
save_settings.update(metadata)
logger_config.info(f"Converting to {target_format} with maximum 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)
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")