certflow.services.printer.template_manager 源代码

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