以下是file_compositor.py的完整代码:#文件合成模块
import os
import tempfile
import logging
import csv
import cv2
from PIL import Image, ImageDraw, ImageFont
from pypdf import PdfReader, PdfWriter
from core.font_manager import FontManager
from utils.image_utils import pdf_to_image
from typing import List, Dict, Tuple, Union, Optional, Any
# 尝试提前导入,避免运行时导入带来的警告
try:
import qrcode
except ImportError:
qrcode = None
logger = logging.getLogger(__name__)
def _generate_qrcode(data: str, region: Dict[str, Any]):
"""生成二维码
:param data: 二维码数据
:param region: 区域配置
:return: PIL 图像对象
"""
if not qrcode:
raise ImportError("qrcode 模块未安装,请先通过 pip install qrcode 安装")
# 获取二维码大小(使用区域宽度)
size = int(region.get('rect').get('width', lambda: 100)()) # 假设 rect 是 QGIS 或 PyQt 的 Rect 对象模拟字典行为
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=0,
)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
return img.resize((size, size))
class FileCompositor:
def __init__(self, data_path: Optional[str] = None, font_manager: Optional[FontManager] = None):
self.data_path = data_path or os.path.join(os.path.dirname(__file__), "../../../data")
self.font_manager = font_manager or FontManager(self.data_path)
self.temp_dir = tempfile.mkdtemp(prefix="auto_print_")
logger.info(f"合成引擎初始化,临时目录: {self.temp_dir}")
def composite(
self,
template_path: str,
regions: List[Dict[str, Any]],
mapping: Dict[str, str],
data_source: Union[str, Any], # DataFrame 或 CSV路径
output_path: str,
logo_path: Optional[str] = None,
spot_color_path: Optional[str] = None
) -> Tuple[bool, List[str]]:
"""
执行合成操作
:param template_path: 模板文件路径
:param regions: 区域配置列表
:param mapping: 字段映射字典 {区域名: 字段名}
:param data_source: 数据源(CSV文件路径或DataFrame)
:param output_path: 输出文件路径
:param logo_path: Logo文件路径(可选)
:param spot_color_path: 专色层文件路径(可选)
:return: (成功标志, 日志信息)
"""
logs: List[str] = []
success = True
# 解析数据源
try:
data_rows = self._parse_data_source(data_source)
logs.append(f"加载数据: {len(data_rows)}行记录")
except Exception as e:
logs.append(f"数据加载失败: {str(e)}")
return False, logs
# 准备输出PDF
output_pdf = PdfWriter()
total_pages = len(data_rows)
# 处理每一行数据
for i, row in enumerate(data_rows):
try:
# 创建当前页
page_image = self._create_page(
template_path, regions, mapping, row, logo_path, spot_color_path
)
# 保存为PDF页面
page_path = os.path.join(self.temp_dir, f"page_{i}.pdf")
page_image.save(page_path, "PDF", resolution=300)
# 添加到输出PDF
with open(page_path, "rb") as f:
page_reader = PdfReader(f)
output_pdf.add_page(page_reader.pages[0])
logs.append(f"页面 {i + 1}/{total_pages} 合成成功")
except Exception as e:
logs.append(f"页面 {i + 1} 合成失败: {str(e)}")
success = False
# 保存最终PDF
if success:
try:
with open(output_path, "wb") as f:
output_pdf.write(f)
logs.append(f"输出文件已保存: {output_path}")
except (IOError, OSError, PermissionError) as e:
logs.append(f"保存输出文件失败: {str(e)}")
success = False
return success, logs
def _parse_data_source(self, data_source: Union[str, Any]) -> List[Dict[str, Any]]:
"""解析数据源为行记录列表
:param data_source: 数据源(CSV文件路径或DataFrame)
:return: 数据行列表
"""
if isinstance(data_source, str):
with open(data_source, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
return list(reader)
elif hasattr(data_source, 'to_dict'): # DataFrame
return data_source.to_dict('records')
else:
raise ValueError("不支持的数据源类型")
def _create_page(
self,
template_path: str,
regions: List[Dict[str, Any]],
mapping: Dict[str, str],
data_row: Dict[str, str],
logo_path: Optional[str],
spot_color_path: Optional[str]
) -> Image.Image:
"""创建单个页面图像
:param template_path: 模板文件路径
:param regions: 区域配置列表
:param mapping: 字段映射字典
:param data_row: 单行数据
:param logo_path: Logo文件路径
:param spot_color_path: 专色层文件路径
:return: PIL图像对象
"""
# 加载模板
if template_path.lower().endswith('.pdf'):
template_img = pdf_to_image(template_path)
if template_img is None:
raise ValueError("无法加载PDF模板")
template_pil = Image.fromarray(cv2.cvtColor(template_img, cv2.COLOR_BGR2RGB))
else:
template_pil = Image.open(template_path)
canvas = template_pil.copy()
# 应用专白层(如果有)
if spot_color_path:
spot_color_img = self._load_spot_color(spot_color_path)
if spot_color_img:
canvas = self._apply_spot_color(canvas, spot_color_img)
# 应用Logo(如果有)
if logo_path:
logo_img = self._load_logo(logo_path)
if logo_img:
canvas.paste(logo_img, (50, 50), logo_img)
draw = ImageDraw.Draw(canvas)
# 处理所有区域
for region in regions:
region_name = region.get('name', '未命名区域')
region_type = region.get('type')
rect = region.get('rect', {})
pos = region.get('pos', (0, 0))
field_name = mapping.get(region_name)
if not field_name:
continue
value = data_row.get(field_name, "")
if not value:
continue
x = rect.get('x', lambda: 0)() + pos[0] if isinstance(pos, tuple) else 0
y = rect.get('y', lambda: 0)() + pos[1] if isinstance(pos, tuple) else 0
width = rect.get('width', lambda: 100)()
height = rect.get('height', lambda: 100)()
# 根据区域类型处理
if region_type == 'text':
self._render_text(draw, value, (int(x), int(y)), region, data_row)
elif region_type == 'qrcode':
try:
qr_img = _generate_qrcode(value, region)
canvas.paste(qr_img, (int(x), int(y)))
except ImportError as e:
logger.warning(f"二维码渲染失败: 缺少模块 {e}")
elif region_type == 'image':
img = self._load_image(value)
if img:
img = self._resize_image(img, (int(width), int(height)))
canvas.paste(img, (int(x), int(y)))
return canvas
def _render_text(
self,
draw: ImageDraw.ImageDraw,
text: str,
position: Tuple[int, int],
region: Dict[str, Any],
data_row: Dict[str, str]
):
"""渲染文本区域
:param draw: ImageDraw 对象
:param text: 要渲染的文本
:param position: 文本位置 (x, y)
:param region: 区域配置字典
:param data_row: 当前数据行
"""
# 获取字体配置
font_name = region.get('font', 'SimHei')
font_size = region.get('font_size', 12)
# 获取字体对象
font = self.font_manager.get_font(f"{font_name},{font_size}")
if not font:
logger.warning(f"字体加载失败: {font_name}, 将使用默认字体")
font = ImageFont.load_default()
# 获取颜色
color_str = region.get('color', "0,0,0")
font_color = self._parse_color(color_str)
# 动态字段颜色
color_field = region.get('color_field')
if color_field and color_field in data_row:
font_color = self._parse_color(data_row[color_field])
# 绘制文本
draw.text(position, text, fill=font_color, font=font)
@staticmethod
def _load_image(image_path: str) -> Optional[Image.Image]:
"""加载图片"""
if not os.path.exists(image_path):
return None
try:
return Image.open(image_path)
except Exception as e:
logging.error(f"加载图片失败: {str(e)}")
return None
@staticmethod
def _resize_image(img: Image.Image, size: Tuple[int, int]) -> Image.Image:
"""调整图片大小"""
try:
resample_method = Image.Resampling.LANCZOS
except AttributeError:
resample_method = Image.LANCZOS
return img.resize(size, resample_method)
@staticmethod
def _load_spot_color(path: str) -> Optional[Image.Image]:
"""加载专色层"""
try:
return Image.open(path)
except Exception as e:
logging.error(f"加载专色层失败: {str(e)}")
return None
@staticmethod
def _apply_spot_color(
canvas: Image.Image,
spot_img: Image.Image
) -> Image.Image:
"""应用专色层到画布"""
if spot_img.size != canvas.size:
try:
resample_method = Image.Resampling.LANCZOS
except AttributeError:
resample_method = Image.LANCZOS
spot_img = spot_img.resize(canvas.size, resample_method)
mask = spot_img.convert("L")
canvas.paste(spot_img, (0, 0), mask)
return canvas
@staticmethod
def _load_logo(path: str) -> Optional[Image.Image]:
"""加载Logo"""
try:
logo = Image.open(path)
if logo.mode != 'RGBA':
logo = logo.convert('RGBA')
return logo
except Exception as e:
logging.error(f"加载Logo失败: {str(e)}")
return None
@staticmethod
def _parse_color(color_str: str) -> Tuple[int, int, int]:
"""解析颜色字符串为RGB元组"""
if color_str.startswith('#'):
hex_color = color_str.lstrip('#')
return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
else:
return tuple(map(int, color_str.split(','))) # type: ignore
运行后出现以下警告,请帮我逐一进行解决,并输出完整的修改后的代码