138 lines
5.1 KiB
Python
138 lines
5.1 KiB
Python
|
|
"""
|
||
|
|
Asset transformation functionality for Issue #144.
|
||
|
|
|
||
|
|
This module provides asset transformation and thumbnail generation capabilities.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import List, Dict, Any, Optional, Tuple
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from PIL import Image
|
||
|
|
import io
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class TransformationResult:
|
||
|
|
"""Result of an asset transformation operation."""
|
||
|
|
success: bool
|
||
|
|
source_path: Path
|
||
|
|
output_path: Path
|
||
|
|
original_size: int
|
||
|
|
transformed_size: int
|
||
|
|
transformation_type: str
|
||
|
|
error_message: Optional[str] = None
|
||
|
|
|
||
|
|
|
||
|
|
class AssetTransformer:
|
||
|
|
"""Transforms assets between formats and sizes."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
"""Initialize the asset transformer."""
|
||
|
|
self.supported_formats = {
|
||
|
|
'image': ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'],
|
||
|
|
'document': ['.pdf', '.docx', '.txt', '.md'],
|
||
|
|
}
|
||
|
|
|
||
|
|
def transform_image(self, source_path: Path, output_path: Path,
|
||
|
|
width: Optional[int] = None, height: Optional[int] = None,
|
||
|
|
format: Optional[str] = None, quality: int = 85) -> TransformationResult:
|
||
|
|
"""Transform an image file."""
|
||
|
|
try:
|
||
|
|
with Image.open(source_path) as img:
|
||
|
|
original_size = source_path.stat().st_size
|
||
|
|
|
||
|
|
# Resize if dimensions provided
|
||
|
|
if width or height:
|
||
|
|
img = img.resize((width or img.width, height or img.height), Image.Resampling.LANCZOS)
|
||
|
|
|
||
|
|
# Save with specified format or keep original
|
||
|
|
save_format = format or img.format
|
||
|
|
img.save(output_path, format=save_format, quality=quality)
|
||
|
|
|
||
|
|
transformed_size = output_path.stat().st_size
|
||
|
|
|
||
|
|
return TransformationResult(
|
||
|
|
success=True,
|
||
|
|
source_path=source_path,
|
||
|
|
output_path=output_path,
|
||
|
|
original_size=original_size,
|
||
|
|
transformed_size=transformed_size,
|
||
|
|
transformation_type=f"resize_{width}x{height}" if (width or height) else "format_conversion"
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
return TransformationResult(
|
||
|
|
success=False,
|
||
|
|
source_path=source_path,
|
||
|
|
output_path=output_path,
|
||
|
|
original_size=0,
|
||
|
|
transformed_size=0,
|
||
|
|
transformation_type="failed",
|
||
|
|
error_message=str(e)
|
||
|
|
)
|
||
|
|
|
||
|
|
def generate_thumbnail(self, source_path: Path, output_path: Path,
|
||
|
|
size: Optional[Tuple[int, int]] = None) -> TransformationResult:
|
||
|
|
"""Generate a thumbnail for the given asset."""
|
||
|
|
size = size or (150, 150)
|
||
|
|
return self.transform_image(
|
||
|
|
source_path, output_path,
|
||
|
|
width=size[0], height=size[1],
|
||
|
|
format='JPEG', quality=80
|
||
|
|
)
|
||
|
|
|
||
|
|
def generate_resolution_variants(self, source_path: Path, output_dir: Path,
|
||
|
|
sizes: Optional[List[Tuple[int, int]]] = None) -> List[TransformationResult]:
|
||
|
|
"""Generate multiple resolution variants of an image."""
|
||
|
|
if sizes is None:
|
||
|
|
sizes = [(150, 150), (300, 300), (600, 600), (1200, 1200)]
|
||
|
|
|
||
|
|
results = []
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
for size in sizes:
|
||
|
|
variant_name = f"{source_path.stem}_{size[0]}x{size[1]}{source_path.suffix}"
|
||
|
|
output_path = output_dir / variant_name
|
||
|
|
result = self.transform_image(source_path, output_path,
|
||
|
|
width=size[0], height=size[1])
|
||
|
|
results.append(result)
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
|
||
|
|
class ThumbnailGenerator:
|
||
|
|
"""Generates thumbnails for various asset types."""
|
||
|
|
|
||
|
|
def __init__(self, default_size: Tuple[int, int] = (150, 150)):
|
||
|
|
"""Initialize thumbnail generator."""
|
||
|
|
self.default_size = default_size
|
||
|
|
self._transformer = None
|
||
|
|
|
||
|
|
@property
|
||
|
|
def transformer(self):
|
||
|
|
if self._transformer is None:
|
||
|
|
self._transformer = AssetTransformer()
|
||
|
|
return self._transformer
|
||
|
|
|
||
|
|
def generate_thumbnail(self, source_path: Path, output_path: Path,
|
||
|
|
size: Optional[Tuple[int, int]] = None) -> TransformationResult:
|
||
|
|
"""Generate a thumbnail for the given asset."""
|
||
|
|
size = size or self.default_size
|
||
|
|
return self.transformer.transform_image(
|
||
|
|
source_path, output_path,
|
||
|
|
width=size[0], height=size[1],
|
||
|
|
format='JPEG', quality=80
|
||
|
|
)
|
||
|
|
|
||
|
|
def generate_thumbnails_batch(self, source_paths: List[Path],
|
||
|
|
output_dir: Path,
|
||
|
|
size: Optional[Tuple[int, int]] = None) -> List[TransformationResult]:
|
||
|
|
"""Generate thumbnails for multiple assets."""
|
||
|
|
results = []
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
for source_path in source_paths:
|
||
|
|
output_path = output_dir / f"{source_path.stem}_thumb.jpg"
|
||
|
|
result = self.generate_thumbnail(source_path, output_path, size)
|
||
|
|
results.append(result)
|
||
|
|
|
||
|
|
return results
|