356 lines
11 KiB
Python
356 lines
11 KiB
Python
"""
|
||
图片处理模块
|
||
|
||
负责图片文件的处理,包括图片读取、尺寸调整、格式转换和对齐设置等功能。
|
||
"""
|
||
|
||
import os
|
||
from typing import Tuple, Optional
|
||
from PIL import Image
|
||
from docx.shared import Inches
|
||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||
from config import config
|
||
|
||
|
||
class ImageProcessor:
|
||
"""图片处理器类"""
|
||
|
||
@staticmethod
|
||
def process_image(image_path: str) -> Tuple[Image.Image, float]:
|
||
"""
|
||
处理图片,包括方向矫正和尺寸调整
|
||
|
||
Args:
|
||
image_path: 图片文件路径
|
||
|
||
Returns:
|
||
Tuple[Image.Image, float]: 处理后的图片对象和宽度(英寸)
|
||
|
||
Raises:
|
||
Exception: 处理图片失败时
|
||
"""
|
||
if not os.path.exists(image_path):
|
||
raise Exception(f"图片文件不存在: {image_path}")
|
||
|
||
try:
|
||
with Image.open(image_path) as img:
|
||
# 处理图片方向(EXIF旋转信息)
|
||
img = ImageProcessor._fix_image_orientation(img)
|
||
|
||
# 调整图片尺寸
|
||
img, width_inches = ImageProcessor._resize_image(img)
|
||
|
||
return img, width_inches
|
||
|
||
except Exception as e:
|
||
raise Exception(f"处理图片失败 {image_path}: {str(e)}")
|
||
|
||
@staticmethod
|
||
def _fix_image_orientation(img: Image.Image) -> Image.Image:
|
||
"""
|
||
根据EXIF信息修正图片方向
|
||
|
||
Args:
|
||
img: PIL图片对象
|
||
|
||
Returns:
|
||
Image.Image: 方向修正后的图片
|
||
"""
|
||
try:
|
||
# 检查是否有EXIF数据
|
||
if hasattr(img, '_getexif'):
|
||
exif = img._getexif()
|
||
if exif is not None:
|
||
# EXIF方向标签
|
||
orientation_tag = 274
|
||
if orientation_tag in exif:
|
||
orientation = exif[orientation_tag]
|
||
|
||
# 根据方向值进行旋转
|
||
if orientation == 3:
|
||
img = img.rotate(180, expand=True)
|
||
elif orientation == 6:
|
||
img = img.rotate(270, expand=True)
|
||
elif orientation == 8:
|
||
img = img.rotate(90, expand=True)
|
||
except Exception as e:
|
||
print(f"修正图片方向时出错: {e}")
|
||
|
||
return img
|
||
|
||
@staticmethod
|
||
def _resize_image(img: Image.Image) -> Tuple[Image.Image, float]:
|
||
"""
|
||
根据配置调整图片尺寸
|
||
|
||
Args:
|
||
img: PIL图片对象
|
||
|
||
Returns:
|
||
Tuple[Image.Image, float]: 调整后的图片和宽度(英寸)
|
||
"""
|
||
if config.image_resize == "width" and config.image_width > 0:
|
||
# 按指定宽度调整
|
||
target_width_px = config.image_width * 96 # 96 DPI
|
||
width, height = img.size
|
||
|
||
if width > target_width_px:
|
||
ratio = target_width_px / width
|
||
new_height = int(height * ratio)
|
||
img = img.resize((int(target_width_px), new_height), Image.LANCZOS)
|
||
|
||
return img, config.image_width
|
||
else:
|
||
# 不调整尺寸,计算当前宽度(英寸)
|
||
width_inches = img.width / 96 # 假设96 DPI
|
||
return img, width_inches
|
||
|
||
@staticmethod
|
||
def get_image_alignment():
|
||
"""
|
||
获取图片对齐方式的Word枚举值
|
||
|
||
Returns:
|
||
WD_ALIGN_PARAGRAPH: Word对齐方式枚举
|
||
"""
|
||
alignment_map = {
|
||
"left": WD_ALIGN_PARAGRAPH.LEFT,
|
||
"center": WD_ALIGN_PARAGRAPH.CENTER,
|
||
"right": WD_ALIGN_PARAGRAPH.RIGHT
|
||
}
|
||
|
||
return alignment_map.get(config.image_alignment, WD_ALIGN_PARAGRAPH.CENTER)
|
||
|
||
@staticmethod
|
||
def validate_image(image_path: str) -> dict:
|
||
"""
|
||
验证图片文件的有效性
|
||
|
||
Args:
|
||
image_path: 图片文件路径
|
||
|
||
Returns:
|
||
dict: 验证结果,包含有效性、错误信息和图片信息
|
||
"""
|
||
result = {
|
||
"valid": False,
|
||
"error": None,
|
||
"info": {}
|
||
}
|
||
|
||
if not os.path.exists(image_path):
|
||
result["error"] = "文件不存在"
|
||
return result
|
||
|
||
try:
|
||
with Image.open(image_path) as img:
|
||
result["valid"] = True
|
||
result["info"] = {
|
||
"format": img.format,
|
||
"mode": img.mode,
|
||
"size": img.size,
|
||
"width": img.width,
|
||
"height": img.height
|
||
}
|
||
|
||
# 检查图片是否过大
|
||
if img.width > 10000 or img.height > 10000:
|
||
result["error"] = "图片尺寸过大"
|
||
result["valid"] = False
|
||
|
||
except Exception as e:
|
||
result["error"] = f"无法打开图片: {str(e)}"
|
||
|
||
return result
|
||
|
||
@staticmethod
|
||
def get_supported_formats() -> list:
|
||
"""
|
||
获取支持的图片格式列表
|
||
|
||
Returns:
|
||
list: 支持的图片格式扩展名列表
|
||
"""
|
||
return ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp', '.tiff']
|
||
|
||
@staticmethod
|
||
def convert_image_format(image_path: str, target_format: str, output_path: str) -> bool:
|
||
"""
|
||
转换图片格式
|
||
|
||
Args:
|
||
image_path: 源图片路径
|
||
target_format: 目标格式(如'PNG', 'JPEG')
|
||
output_path: 输出文件路径
|
||
|
||
Returns:
|
||
bool: 是否转换成功
|
||
"""
|
||
try:
|
||
with Image.open(image_path) as img:
|
||
# 如果是JPEG格式且原图有透明通道,转为RGB
|
||
if target_format.upper() == 'JPEG' and img.mode in ('RGBA', 'LA'):
|
||
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
|
||
rgb_img.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
|
||
img = rgb_img
|
||
|
||
img.save(output_path, format=target_format)
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"转换图片格式失败: {e}")
|
||
return False
|
||
|
||
@staticmethod
|
||
def create_thumbnail(image_path: str, thumbnail_path: str, size: Tuple[int, int] = (200, 200)) -> bool:
|
||
"""
|
||
创建图片缩略图
|
||
|
||
Args:
|
||
image_path: 源图片路径
|
||
thumbnail_path: 缩略图保存路径
|
||
size: 缩略图尺寸(宽度, 高度)
|
||
|
||
Returns:
|
||
bool: 是否创建成功
|
||
"""
|
||
try:
|
||
with Image.open(image_path) as img:
|
||
img.thumbnail(size, Image.LANCZOS)
|
||
img.save(thumbnail_path)
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"创建缩略图失败: {e}")
|
||
return False
|
||
|
||
@staticmethod
|
||
def get_image_info(image_path: str) -> Optional[dict]:
|
||
"""
|
||
获取图片详细信息
|
||
|
||
Args:
|
||
image_path: 图片文件路径
|
||
|
||
Returns:
|
||
Optional[dict]: 图片信息字典,失败时返回None
|
||
"""
|
||
try:
|
||
with Image.open(image_path) as img:
|
||
info = {
|
||
"filename": os.path.basename(image_path),
|
||
"format": img.format,
|
||
"mode": img.mode,
|
||
"size": img.size,
|
||
"width": img.width,
|
||
"height": img.height,
|
||
"file_size": os.path.getsize(image_path)
|
||
}
|
||
|
||
# 尝试获取EXIF信息
|
||
if hasattr(img, '_getexif'):
|
||
exif = img._getexif()
|
||
if exif:
|
||
info["has_exif"] = True
|
||
# 获取一些常用的EXIF信息
|
||
orientation = exif.get(274) # 方向
|
||
if orientation:
|
||
info["orientation"] = orientation
|
||
else:
|
||
info["has_exif"] = False
|
||
else:
|
||
info["has_exif"] = False
|
||
|
||
return info
|
||
|
||
except Exception as e:
|
||
print(f"获取图片信息失败: {e}")
|
||
return None
|
||
|
||
@staticmethod
|
||
def batch_validate_images(image_paths: list) -> dict:
|
||
"""
|
||
批量验证图片文件
|
||
|
||
Args:
|
||
image_paths: 图片文件路径列表
|
||
|
||
Returns:
|
||
dict: 验证结果统计
|
||
"""
|
||
result = {
|
||
"total": len(image_paths),
|
||
"valid": 0,
|
||
"invalid": 0,
|
||
"errors": []
|
||
}
|
||
|
||
for image_path in image_paths:
|
||
validation = ImageProcessor.validate_image(image_path)
|
||
if validation["valid"]:
|
||
result["valid"] += 1
|
||
else:
|
||
result["invalid"] += 1
|
||
result["errors"].append({
|
||
"path": image_path,
|
||
"error": validation["error"]
|
||
})
|
||
|
||
return result
|
||
|
||
@staticmethod
|
||
def optimize_image_for_docx(image_path: str, temp_dir: str) -> str:
|
||
"""
|
||
优化图片以适合插入DOCX文档
|
||
|
||
Args:
|
||
image_path: 原图片路径
|
||
temp_dir: 临时文件目录
|
||
|
||
Returns:
|
||
str: 优化后的图片路径
|
||
"""
|
||
try:
|
||
# 确保临时目录存在
|
||
os.makedirs(temp_dir, exist_ok=True)
|
||
|
||
with Image.open(image_path) as img:
|
||
# 修正方向
|
||
img = ImageProcessor._fix_image_orientation(img)
|
||
|
||
# 根据配置调整尺寸
|
||
img, _ = ImageProcessor._resize_image(img)
|
||
|
||
# 生成临时文件路径
|
||
filename = os.path.basename(image_path)
|
||
name, ext = os.path.splitext(filename)
|
||
temp_path = os.path.join(temp_dir, f"{name}_optimized{ext}")
|
||
|
||
# 保存优化后的图片
|
||
# 如果是PNG且没有透明通道,转为JPEG以减少文件大小
|
||
if img.format == 'PNG' and img.mode == 'RGB':
|
||
temp_path = os.path.join(temp_dir, f"{name}_optimized.jpg")
|
||
img.save(temp_path, 'JPEG', quality=85, optimize=True)
|
||
else:
|
||
img.save(temp_path, optimize=True)
|
||
|
||
return temp_path
|
||
|
||
except Exception as e:
|
||
print(f"优化图片失败: {e}")
|
||
return image_path # 返回原路径
|
||
|
||
|
||
# 创建全局图片处理器实例
|
||
image_processor = ImageProcessor()
|
||
|
||
|
||
# 兼容旧接口的函数
|
||
def process_image(image_path: str) -> Tuple[Image.Image, float]:
|
||
"""处理图片(兼容旧接口)"""
|
||
return ImageProcessor.process_image(image_path)
|
||
|
||
|
||
def get_image_alignment():
|
||
"""获取图片对齐方式(兼容旧接口)"""
|
||
return ImageProcessor.get_image_alignment() |