# src/certflow/services/printer/template_manager.py
"""合格证模板管理器
加载和管理从 VBA 坐标转换而来的模板配置。
通过 cfg() 从主配置统一读取,不再独立加载 YAML 文件。
"""
from __future__ import annotations
from typing import TypedDict
from certflow.utils.logger import logger
# ============================================================
# 类型定义
# ============================================================
[文档]
class FieldPosition(TypedDict):
"""字段位置定义"""
x_mm: float
y_mm: float
width_mm: float
height_mm: float
font_size: int
align: str
[文档]
class TemplateInfo(TypedDict):
"""模板信息"""
name: str
variant: str
background: str
page_width: int
page_height: int
fields: dict[str, FieldPosition]
enabled: bool
[文档]
class TemplateManager:
"""合格证模板管理器
负责加载和管理从 VBA 坐标转换而来的模板配置。
通过 cfg() 从主配置统一读取 template_coordinates 节点。
使用示例:
# 初始化管理器
tm = TemplateManager()
# 获取所有模板
templates = tm.get_all_templates()
# 获取指定模板
template = tm.get_template("russian")
# 获取模板中的字段坐标
pos = tm.get_field_position("russian", "product_name")
print(f"产品名称位置: x={pos['x_mm']}mm, y={pos['y_mm']}mm")
"""
def __init__(self, config_path: str | None = None):
"""初始化模板管理器
Args:
config_path: 已废弃,保留仅为向后兼容。模板坐标现在通过 cfg() 统一读取。
"""
self.config_path = config_path # 保留仅为向后兼容
self._templates: dict[str, TemplateInfo] = {}
self._load_config()
def _load_config(self) -> None:
"""从主配置的 template_coordinates 节点加载模板"""
from certflow.config.settings import cfg
templates_data = cfg("print_templates", {})
if not templates_data:
raise FileNotFoundError(
"主配置中缺少 template_coordinates 节点,请确保 config.yaml 中已 !include templates/coordinates.yaml"
)
for key, tmpl_data in templates_data.items():
fields: dict[str, FieldPosition] = {}
for field_name, field_pos in tmpl_data.get("fields", {}).items():
# 处理 YAML 内联格式(pyyaml 应该已解析为 dict)
if isinstance(field_pos, dict):
# 兼容字段名:支持 x/x_mm 两种写法
x_val = field_pos.get("x_mm") or field_pos.get("x", 0)
y_val = field_pos.get("y_mm") or field_pos.get("y", 0)
w_val = field_pos.get("width_mm") or field_pos.get("width", 0)
h_val = field_pos.get("height_mm") or field_pos.get("height", 0)
fields[field_name] = {
"x_mm": float(x_val),
"y_mm": float(y_val),
"width_mm": float(w_val),
"height_mm": float(h_val),
"font_size": int(field_pos.get("font_size", 9)),
"align": str(field_pos.get("align", "left")),
}
elif isinstance(field_pos, str):
# 如果还是字符串,尝试手动解析
# 格式: {x: 36.62, y: 25.27, width: 35.72, height: 8.77, font_size: 10}
try:
# 移除 { } 并按逗号分割
content = field_pos.strip("{}")
parts = content.split(",")
parsed = {}
for part in parts:
if ":" in part:
k, v = part.split(":", 1)
parsed[k.strip()] = float(v.strip())
fields[field_name] = {
"x_mm": parsed.get("x", 0),
"y_mm": parsed.get("y", 0),
"width_mm": parsed.get("width", 0),
"height_mm": parsed.get("height", 0),
"font_size": int(parsed.get("font_size", 9)),
"align": "left",
}
except Exception as e:
print(f"⚠️ 字段 {field_name} 解析失败: {e}")
continue
else:
print(f"⚠️ 字段 {field_name} 格式未知: {type(field_pos)}")
continue
self._templates[key] = {
"name": tmpl_data.get("name", ""),
"variant": tmpl_data.get("variant", ""),
"background": tmpl_data.get("background", ""),
"page_width": tmpl_data.get("page_width", 60),
"page_height": tmpl_data.get("page_height", 100),
"fields": fields,
"enabled": bool(tmpl_data.get("enabled", False)),
}
logger.info(f"模板管理器已加载 {len(self._templates)} 个模板配置")
[文档]
def get_all_templates(self) -> dict[str, TemplateInfo]:
"""获取所有模板
Returns:
模板字典,key 为模板标识,value 为模板信息
"""
return self._templates.copy()
[文档]
def get_template(self, template_key: str) -> TemplateInfo | None:
"""获取指定模板
Args:
template_key: 模板键名(如 "russian", "full_chinese_1")
Returns:
模板信息,不存在时返回 None
"""
return self._templates.get(template_key)
[文档]
def get_field_position(self, template_key: str, field_name: str) -> FieldPosition | None:
"""获取模板中指定字段的位置
Args:
template_key: 模板键名
field_name: 字段名(如 "product_name", "dn")
Returns:
字段位置信息,不存在时返回 None
"""
template = self.get_template(template_key)
if not template:
return None
return template["fields"].get(field_name)
[文档]
def get_field_x(self, template_key: str, field_name: str) -> float | None:
"""获取字段的 X 坐标
Args:
template_key: 模板键名
field_name: 字段名
Returns:
X 坐标(mm),不存在时返回 None
"""
pos = self.get_field_position(template_key, field_name)
return pos["x_mm"] if pos else None
[文档]
def get_field_y(self, template_key: str, field_name: str) -> float | None:
"""获取字段的 Y 坐标
Args:
template_key: 模板键名
field_name: 字段名
Returns:
Y 坐标(mm),不存在时返回 None
"""
pos = self.get_field_position(template_key, field_name)
return pos["y_mm"] if pos else None
[文档]
def get_field_width(self, template_key: str, field_name: str) -> float | None:
"""获取字段的文本框宽度
Args:
template_key: 模板键名
field_name: 字段名
Returns:
宽度(mm),不存在时返回 None
"""
pos = self.get_field_position(template_key, field_name)
return pos["width_mm"] if pos else None
[文档]
def get_field_height(self, template_key: str, field_name: str) -> float | None:
"""获取字段的文本框高度
Args:
template_key: 模板键名
field_name: 字段名
Returns:
高度(mm),不存在时返回 None
"""
pos = self.get_field_position(template_key, field_name)
return pos["height_mm"] if pos else None
[文档]
def get_field_font_size(self, template_key: str, field_name: str) -> int | None:
"""获取字段的字号
Args:
template_key: 模板键名
field_name: 字段名
Returns:
字号(pt),不存在时返回 None
"""
pos = self.get_field_position(template_key, field_name)
return pos["font_size"] if pos else None
[文档]
def get_field_align(self, template_key: str, field_name: str) -> str | None:
"""获取字段的对齐方式
Args:
template_key: 模板键名
field_name: 字段名
Returns:
对齐方式字符串,不存在时返回 None
"""
pos = self.get_field_position(template_key, field_name)
return pos["align"] if pos else None
[文档]
def get_background_path(self, template_key: str) -> str | None:
"""获取模板的底板图片路径
Args:
template_key: 模板键名
Returns:
图片路径字符串,不存在时返回 None
"""
template = self.get_template(template_key)
return template["background"] if template else None
[文档]
def get_page_size(self, template_key: str) -> tuple[int, int]:
"""获取模板的页面尺寸
Returns:
(width_mm, height_mm) 元组
"""
template = self.get_template(template_key)
if template:
return template["page_width"], template["page_height"]
return 60, 100
[文档]
def list_templates(self) -> list[tuple[str, str, str]]:
"""列出所有模板
Returns:
(key, name, variant) 列表
"""
return [(key, tmpl["name"], tmpl["variant"]) for key, tmpl in self._templates.items()]
[文档]
def list_enabled_templates(self) -> list[tuple[str, str, str]]:
"""列出已启用的模板(coordinates.yaml 中 enabled: true),供 UI 下拉使用。
Returns:
(key, name, variant) 列表,仅含 enabled 为真的模板。
"""
return [
(key, tmpl["name"], tmpl["variant"])
for key, tmpl in self._templates.items()
if tmpl.get("enabled", False)
]
[文档]
def print_template_info(self, template_key: str) -> None:
"""打印模板信息(调试用)
Args:
template_key: 模板键名。
"""
template = self.get_template(template_key)
if not template:
print(f"模板 '{template_key}' 不存在")
return
print(f"\n{'=' * 60}")
print(f"模板: {template['name']} - {template['variant']}")
print(f"底板: {template['background']}")
print(f"尺寸: {template['page_width']}x{template['page_height']}mm")
print(f"{'=' * 60}")
print(f"{'字段':<20} {'X':>8} {'Y':>8} {'W':>8} {'H':>8} {'字号':>6}")
print(f"{'-' * 60}")
for field, pos in template["fields"].items():
# 安全获取值
x = pos.get("x_mm", 0) if isinstance(pos, dict) else 0
y = pos.get("y_mm", 0) if isinstance(pos, dict) else 0
w = pos.get("width_mm", 0) if isinstance(pos, dict) else 0
h = pos.get("height_mm", 0) if isinstance(pos, dict) else 0
font = pos.get("font_size", 0) if isinstance(pos, dict) else 0
print(f"{field:<20} {x:>8.2f} {y:>8.2f} {w:>8.2f} {h:>8.2f} {font:>6}")
[文档]
def reload(self) -> None:
"""重新加载模板配置"""
self._templates.clear()
self._load_config()
# ============================================================
# 便捷函数
# ============================================================
[文档]
def get_template_manager(config_path: str | None = None) -> TemplateManager:
"""获取模板管理器单例。
Returns:
TemplateManager: 全局共享的模板管理器实例。
"""
global _template_manager
if _template_manager is None:
_template_manager = TemplateManager(config_path)
return _template_manager
_template_manager: TemplateManager | None = None
# ============================================================
# 测试代码
# ============================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="合格证模板管理器测试")
parser.add_argument("--list", "-l", action="store_true", help="列出所有模板")
parser.add_argument("--show", "-s", type=str, help="显示指定模板信息")
parser.add_argument("--config", "-c", type=str, help="配置文件路径")
args = parser.parse_args()
tm = TemplateManager(args.config)
if args.list:
print("\n可用模板列表:")
print("-" * 60)
for key, name, variant in tm.list_templates():
print(f" {key:<20} {name} - {variant}")
elif args.show:
tm.print_template_info(args.show)
else:
# 默认显示所有模板
print("\n📁 模板坐标来自主配置 template_coordinates 节点")
print(f"📋 加载模板数量: {len(tm.get_all_templates())}")
for key in tm.get_all_templates():
tm.print_template_info(key)