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()
|