Add File
This commit is contained in:
346
src/landppt/services/image/processors/image_processor.py
Normal file
346
src/landppt/services/image/processors/image_processor.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
图片处理器核心类
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Tuple, Dict, Any, List
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import time
|
||||
from PIL import Image, ImageEnhance, ImageFilter, ImageDraw, ImageFont
|
||||
import io
|
||||
|
||||
from ..models import (
|
||||
ImageInfo, ImageMetadata, ImageFormat, ImageProcessingOptions,
|
||||
ImageOperationResult
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
"""图片处理器"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
self.max_width = config.get('max_image_width', 1920)
|
||||
self.max_height = config.get('max_image_height', 1080)
|
||||
self.default_quality = config.get('compression_quality', 85)
|
||||
self.auto_resize = config.get('auto_resize_enabled', True)
|
||||
|
||||
# 支持的格式
|
||||
self.supported_formats = {
|
||||
ImageFormat.JPEG: 'JPEG',
|
||||
ImageFormat.JPG: 'JPEG',
|
||||
ImageFormat.PNG: 'PNG',
|
||||
ImageFormat.GIF: 'GIF',
|
||||
ImageFormat.WEBP: 'WEBP',
|
||||
ImageFormat.BMP: 'BMP',
|
||||
ImageFormat.TIFF: 'TIFF'
|
||||
}
|
||||
|
||||
# 重采样方法映射
|
||||
self.resample_methods = {
|
||||
'lanczos': Image.Resampling.LANCZOS,
|
||||
'bicubic': Image.Resampling.BICUBIC,
|
||||
'bilinear': Image.Resampling.BILINEAR,
|
||||
'nearest': Image.Resampling.NEAREST
|
||||
}
|
||||
|
||||
async def process_image(self,
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
options: Optional[ImageProcessingOptions] = None) -> ImageOperationResult:
|
||||
"""处理图片"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 使用默认选项如果未提供
|
||||
if options is None:
|
||||
options = ImageProcessingOptions()
|
||||
|
||||
# 在线程池中执行图片处理
|
||||
result = await asyncio.get_event_loop().run_in_executor(
|
||||
None, self._process_image_sync, input_path, output_path, options
|
||||
)
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
result.processing_time = processing_time
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Image processing failed: {e}")
|
||||
return ImageOperationResult(
|
||||
success=False,
|
||||
message=f"Image processing failed: {str(e)}",
|
||||
error_code="processing_error",
|
||||
processing_time=time.time() - start_time
|
||||
)
|
||||
|
||||
def _process_image_sync(self,
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
options: ImageProcessingOptions) -> ImageOperationResult:
|
||||
"""同步处理图片"""
|
||||
try:
|
||||
# 打开图片
|
||||
with Image.open(input_path) as img:
|
||||
# 转换为RGB模式(如果需要)
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
if options.output_format in [ImageFormat.JPEG, ImageFormat.JPG]:
|
||||
# JPEG不支持透明度,转换为RGB
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
||||
img = background
|
||||
elif img.mode == 'P':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
# 尺寸调整
|
||||
if options.resize_width or options.resize_height:
|
||||
img = self._resize_image(img, options)
|
||||
elif self.auto_resize and (img.width > self.max_width or img.height > self.max_height):
|
||||
# 自动调整尺寸
|
||||
auto_options = ImageProcessingOptions(
|
||||
resize_width=self.max_width,
|
||||
resize_height=self.max_height,
|
||||
maintain_aspect_ratio=True
|
||||
)
|
||||
img = self._resize_image(img, auto_options)
|
||||
|
||||
# 图片增强
|
||||
if options.auto_enhance or any([
|
||||
options.brightness, options.contrast,
|
||||
options.saturation, options.sharpness
|
||||
]):
|
||||
img = self._enhance_image(img, options)
|
||||
|
||||
# 添加水印
|
||||
if options.add_watermark and options.watermark_text:
|
||||
img = self._add_watermark(img, options)
|
||||
|
||||
# 保存图片
|
||||
save_kwargs = self._get_save_kwargs(options)
|
||||
|
||||
# 确保输出目录存在
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
img.save(output_path, **save_kwargs)
|
||||
|
||||
# 获取处理后的图片信息
|
||||
processed_info = self._get_image_info(output_path)
|
||||
|
||||
return ImageOperationResult(
|
||||
success=True,
|
||||
message="Image processed successfully",
|
||||
image_info=processed_info
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Synchronous image processing failed: {str(e)}")
|
||||
|
||||
def _resize_image(self, img: Image.Image, options: ImageProcessingOptions) -> Image.Image:
|
||||
"""调整图片尺寸"""
|
||||
original_width, original_height = img.size
|
||||
target_width = options.resize_width
|
||||
target_height = options.resize_height
|
||||
|
||||
if options.maintain_aspect_ratio:
|
||||
# 保持宽高比
|
||||
if target_width and target_height:
|
||||
# 计算缩放比例
|
||||
width_ratio = target_width / original_width
|
||||
height_ratio = target_height / original_height
|
||||
ratio = min(width_ratio, height_ratio)
|
||||
|
||||
new_width = int(original_width * ratio)
|
||||
new_height = int(original_height * ratio)
|
||||
elif target_width:
|
||||
ratio = target_width / original_width
|
||||
new_width = target_width
|
||||
new_height = int(original_height * ratio)
|
||||
elif target_height:
|
||||
ratio = target_height / original_height
|
||||
new_width = int(original_width * ratio)
|
||||
new_height = target_height
|
||||
else:
|
||||
return img
|
||||
else:
|
||||
# 不保持宽高比
|
||||
new_width = target_width or original_width
|
||||
new_height = target_height or original_height
|
||||
|
||||
# 获取重采样方法
|
||||
resample = self.resample_methods.get(options.resize_method, Image.Resampling.LANCZOS)
|
||||
|
||||
return img.resize((new_width, new_height), resample)
|
||||
|
||||
def _enhance_image(self, img: Image.Image, options: ImageProcessingOptions) -> Image.Image:
|
||||
"""图片增强"""
|
||||
enhanced_img = img
|
||||
|
||||
# 亮度调整
|
||||
if options.brightness is not None:
|
||||
enhancer = ImageEnhance.Brightness(enhanced_img)
|
||||
enhanced_img = enhancer.enhance(options.brightness)
|
||||
|
||||
# 对比度调整
|
||||
if options.contrast is not None:
|
||||
enhancer = ImageEnhance.Contrast(enhanced_img)
|
||||
enhanced_img = enhancer.enhance(options.contrast)
|
||||
|
||||
# 饱和度调整
|
||||
if options.saturation is not None:
|
||||
enhancer = ImageEnhance.Color(enhanced_img)
|
||||
enhanced_img = enhancer.enhance(options.saturation)
|
||||
|
||||
# 锐度调整
|
||||
if options.sharpness is not None:
|
||||
enhancer = ImageEnhance.Sharpness(enhanced_img)
|
||||
enhanced_img = enhancer.enhance(options.sharpness)
|
||||
|
||||
# 自动增强
|
||||
if options.auto_enhance:
|
||||
# 简单的自动增强:轻微增加对比度和锐度
|
||||
contrast_enhancer = ImageEnhance.Contrast(enhanced_img)
|
||||
enhanced_img = contrast_enhancer.enhance(1.1)
|
||||
|
||||
sharpness_enhancer = ImageEnhance.Sharpness(enhanced_img)
|
||||
enhanced_img = sharpness_enhancer.enhance(1.1)
|
||||
|
||||
return enhanced_img
|
||||
|
||||
def _add_watermark(self, img: Image.Image, options: ImageProcessingOptions) -> Image.Image:
|
||||
"""添加水印"""
|
||||
try:
|
||||
# 创建水印图层
|
||||
watermark = Image.new('RGBA', img.size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(watermark)
|
||||
|
||||
# 尝试加载字体
|
||||
try:
|
||||
font_size = max(20, min(img.width, img.height) // 40)
|
||||
font = ImageFont.truetype("arial.ttf", font_size)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# 获取文本尺寸
|
||||
text = options.watermark_text
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
# 计算水印位置
|
||||
margin = 20
|
||||
positions = {
|
||||
'top_left': (margin, margin),
|
||||
'top_right': (img.width - text_width - margin, margin),
|
||||
'bottom_left': (margin, img.height - text_height - margin),
|
||||
'bottom_right': (img.width - text_width - margin, img.height - text_height - margin),
|
||||
'center': ((img.width - text_width) // 2, (img.height - text_height) // 2)
|
||||
}
|
||||
|
||||
position = positions.get(options.watermark_position, positions['bottom_right'])
|
||||
|
||||
# 绘制水印
|
||||
alpha = int(255 * options.watermark_opacity)
|
||||
draw.text(position, text, font=font, fill=(255, 255, 255, alpha))
|
||||
|
||||
# 合并水印
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
return Image.alpha_composite(img, watermark)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to add watermark: {e}")
|
||||
return img
|
||||
|
||||
def _get_save_kwargs(self, options: ImageProcessingOptions) -> Dict[str, Any]:
|
||||
"""获取保存参数"""
|
||||
kwargs = {}
|
||||
|
||||
# 质量设置
|
||||
quality = options.quality or self.default_quality
|
||||
if options.output_format in [ImageFormat.JPEG, ImageFormat.JPG, ImageFormat.WEBP]:
|
||||
kwargs['quality'] = quality
|
||||
kwargs['optimize'] = options.optimize
|
||||
|
||||
if options.output_format in [ImageFormat.JPEG, ImageFormat.JPG]:
|
||||
kwargs['progressive'] = options.progressive
|
||||
|
||||
# PNG优化
|
||||
elif options.output_format == ImageFormat.PNG:
|
||||
kwargs['optimize'] = options.optimize
|
||||
|
||||
return kwargs
|
||||
|
||||
def _get_image_info(self, image_path: Path) -> ImageInfo:
|
||||
"""获取图片信息"""
|
||||
with Image.open(image_path) as img:
|
||||
# 基本信息
|
||||
width, height = img.size
|
||||
format_name = img.format.lower() if img.format else 'unknown'
|
||||
file_size = image_path.stat().st_size
|
||||
|
||||
# 创建元数据
|
||||
metadata = ImageMetadata(
|
||||
width=width,
|
||||
height=height,
|
||||
format=ImageFormat(format_name) if format_name in [f.value for f in ImageFormat] else ImageFormat.JPEG,
|
||||
file_size=file_size,
|
||||
color_mode=img.mode,
|
||||
has_transparency=img.mode in ('RGBA', 'LA', 'P')
|
||||
)
|
||||
|
||||
# 生成图片ID
|
||||
image_id = self._generate_image_id(image_path)
|
||||
|
||||
return ImageInfo(
|
||||
image_id=image_id,
|
||||
source_type="processed",
|
||||
provider="image_processor",
|
||||
local_path=str(image_path),
|
||||
filename=image_path.name,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
def _generate_image_id(self, image_path: Path) -> str:
|
||||
"""生成图片ID"""
|
||||
# 使用文件路径和修改时间生成唯一ID
|
||||
stat = image_path.stat()
|
||||
content = f"{image_path}_{stat.st_mtime}_{stat.st_size}"
|
||||
return hashlib.md5(content.encode()).hexdigest()
|
||||
|
||||
async def get_image_metadata(self, image_path: Path) -> ImageMetadata:
|
||||
"""获取图片元数据"""
|
||||
def _get_metadata():
|
||||
with Image.open(image_path) as img:
|
||||
return ImageMetadata(
|
||||
width=img.width,
|
||||
height=img.height,
|
||||
format=ImageFormat(img.format.lower()) if img.format else ImageFormat.JPEG,
|
||||
file_size=image_path.stat().st_size,
|
||||
color_mode=img.mode,
|
||||
has_transparency=img.mode in ('RGBA', 'LA', 'P')
|
||||
)
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(None, _get_metadata)
|
||||
|
||||
async def create_thumbnail(self,
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
size: Tuple[int, int] = (200, 200)) -> ImageOperationResult:
|
||||
"""创建缩略图"""
|
||||
options = ImageProcessingOptions(
|
||||
resize_width=size[0],
|
||||
resize_height=size[1],
|
||||
maintain_aspect_ratio=True,
|
||||
quality=80,
|
||||
optimize=True
|
||||
)
|
||||
|
||||
return await self.process_image(input_path, output_path, options)
|
||||
Reference in New Issue
Block a user