TxT2Docx/image_processor.py
2025-09-21 19:01:40 +08:00

356 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
图片处理模块
负责图片文件的处理,包括图片读取、尺寸调整、格式转换和对齐设置等功能。
"""
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()