certflow.services.printer.template_field_formatter 源代码

"""模板差异化字段格式化(docs/PRINT_BLUEPRINT.md §4)。

按模板语言族(全中文/中英文/全英文/俄英文)把原始字段值渲染为打印文本:
- 全中文底板已预印 ≤℃ / mm·in / MPa·Lb → 只打数值;英制需 X 偏移。
- 中英文/全英文/俄英文底板无单位 → 自带单位。

``field_display_formats`` 配置位于 ``config/imperial_rules.yaml``(该文件未被主配置
``!include``,故此处直接加载;同文件的 ``pn_display_formats`` 此前为死配置,本类一并接管)。

``format()`` 返回 ``(渲染文本, 英制 X 偏移 mm)``。打印引擎对非零 ``x_offset`` 应将该字段
坐标右移;现已接入 ESC/P 引擎:``PrintView._format_print_data`` 将偏移随数据以
``_x_offsets`` 透传,``PrinterService.print_by_template_config`` /
``print_by_template`` 据此右移字段 X 坐标(蓝图 §4.2)。
"""

from __future__ import annotations

import re
from functools import lru_cache
from typing import Any

import yaml

from certflow.utils.logger import logger
from certflow.utils.path_utils import get_project_root

_NUMBER_RE = re.compile(r"[-+]?\d*\.?\d+")
_IMPERIAL_PN_RE = re.compile(r"(?i)lb")  # 含 Lb 即英制磅级
_IMPERIAL_DN_RE = re.compile(r"(?i)in\s*$")  # 以 in 结尾即英制口径

# 配置键前缀 → 语言族(与 vba_mapping.get_style_code 对齐;打印/扫描/预览共用单一真相)
_FAMILY_BY_PREFIX: tuple[tuple[str, str], ...] = (
    ("full_english", "全英文"),
    ("full_chinese", "全中文"),
    ("bilingual", "中英文"),
    ("russian", "俄英文"),
)
# 已知语言族(避免把族串「中英文」误判为默认「全中文」)
_KNOWN_FAMILIES: frozenset[str] = frozenset({"全中文", "中英文", "全英文", "俄英文"})


[文档] def family_of(key: str | None) -> str: """由模板配置键或语言族串推导语言族(全中文/中英文/全英文/俄英文)。 单一真相来源:``print_view`` / ``certificate_preview_painter`` / ``scan_service`` 均应收口到此函数,消除重复的族判定逻辑(蓝图 §9-3 相关清理)。 """ key = key or "" if key in _KNOWN_FAMILIES: return key for prefix, fam in _FAMILY_BY_PREFIX: if key.startswith(prefix): return fam return "全中文"
[文档] def resolve_template_key(value: str | None) -> str: """由模板语言族/标签/配置键解析 ``coordinates.yaml`` 的模板键。 用于队列打印按证书模板类型选取正确坐标源(取代硬编码 positions): - 已是合法键 → 直接返回; - 该语言族 ``enabled`` 变体优先(如 全中文 → full_chinese_3); - 否则取该族首个变体; - 兜底 full_chinese_3 → 首个键。 """ from certflow.config.settings import cfg templates = cfg("print_templates", {}) or {} if not templates: return "full_chinese_3" if value in templates: return value family = family_of(value) enabled = [ k for k, v in templates.items() if isinstance(v, dict) and v.get("enabled") and family_of(k) == family ] if enabled: return enabled[0] any_family = [k for k, v in templates.items() if isinstance(v, dict) and family_of(k) == family] if any_family: return any_family[0] if "full_chinese_3" in templates: return "full_chinese_3" return next(iter(templates))
[文档] class TemplateFieldFormatter: """按模板语言族渲染字段打印文本。""" def __init__(self, formats: dict[str, dict[str, dict[str, Any]]] | None = None) -> None: self._formats = formats or {}
[文档] @classmethod def load(cls) -> TemplateFieldFormatter: """从 config/imperial_rules.yaml 加载 field_display_formats。""" path = get_project_root() / "config" / "imperial_rules.yaml" try: data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} except FileNotFoundError: logger.warning("[TemplateFieldFormatter] 未找到 imperial_rules.yaml,使用空配置") return cls({}) formats = data.get("field_display_formats") or {} return cls(formats)
@staticmethod def _extract_number(raw: str) -> float | None: """从原始值中提取首个数值(剥离 ≤ / ℃ / MPa / Lb / DN 等符号)。""" m = _NUMBER_RE.search(raw) if not m: return None try: val = float(m.group(0)) return int(val) if val.is_integer() else val except ValueError: return None @staticmethod def _is_imperial(field: str, raw: str) -> bool: """判断该字段值是否为英制(pn 含 Lb;dn 含 in)。""" if field == "pn": return bool(_IMPERIAL_PN_RE.search(raw)) if field == "dn": return bool(_IMPERIAL_DN_RE.search(raw)) return False
[文档] def format(self, field: str, raw: Any, template_type: str) -> tuple[str, float]: """渲染字段。 Args: field: 字段名(pn/temperature/medium/dn) raw: 原始值(可能带符号,如 ≤425℃ / 2.5MPa / 150Lb / DN200) template_type: 模板语言族(全中文/中英文/全英文/俄英文) Returns: (渲染文本, 英制 X 偏移 mm) """ cfg = (self._formats.get(template_type, {}) or {}).get(field) if not cfg: return (str(raw) if raw is not None else "", 0.0) raw = str(raw).strip() if not raw: return ("", 0.0) imperial = self._is_imperial(field, raw) num = self._extract_number(raw) if num is None: return (raw, 0.0) if imperial and cfg.get("imperial_pattern"): pat = cfg["imperial_pattern"] # 英制保留单位文本(如中文 pn 的 "{value}Lb")→ 不右移,整串打印在常规位置; # 英制仅打数值(如 "{value}")→ 右移,使数值落在预印单位标签下方。 x_off = ( float(cfg.get("imperial_x_offset_mm", 0) or 0) if pat.strip() == "{value}" else 0.0 ) else: pat = cfg.get("pattern", "{value}") x_off = 0.0 try: text = pat.format(value=num) except (ValueError, KeyError): text = raw return (text, x_off)
[文档] def format_pn(self, value: Any, unit: Any, family: str) -> str: """公称压力格式化(打印/扫描/预览共用,``(value, unit)`` 拆分自权威串 ``pn_display``)。 与打印侧 ``format("pn", ...)`` 同源;``pn_display`` 为权威输入(任意串), ``pn_value`` / ``pn_unit`` 由其反向拆出。门限(中文可无单位、非中文必须带单位) 由 ``output_guard.validate_pn_text`` 把关,此处只负责「按模板语言族决定带不带单位」: - 空值 → ``""``; - 非标准单位(KPa 等,不在 ``{None, MPa, Lb}``)→ 原值 ``f"{value}{unit}"``, 不丢单位、不做单位换算; - ``MPa`` 整数补一位小数(如 ``32`` → ``32.0``); - 英制 ``lb``/``Lb`` 大小写不敏感,统一展示为 ``Lb``(如 ``150Lb``); - 全中文族:底板预印 ``MPa`` → 仅返回数值(如 ``25.0MPa`` → ``25.0``); 但若单位为非 ``MPa``(如英制磅级 ``Lb`` 用中文合格证打印),为避免与预印 ``MPa`` 混淆,**保留单位**(如 ``1500Lb`` → ``1500Lb``); - 中英文/全英文/俄英文族:必须带单位(如 ``32.0MPa`` / ``150Lb``)。 """ if value is None: return "" val = str(value).strip() if not val: return "" if unit not in (None, "", "MPa", "Lb", "lb"): return f"{val}{unit or ''}" # MPa 或中文默认(无单位视为 MPa)→ 整数补一位小数(如 ``32`` → ``32.0``) if unit in (None, "", "MPa"): try: f = float(val) if f == int(f): val = f"{f:.1f}" except ValueError: pass is_cn = family == "全中文" or str(family).startswith("full_chinese") display_unit = "Lb" if str(unit).lower() == "lb" else (unit or "") if is_cn: # 中文底板预印 MPa → 不带头;非 MPa 单位(英制 Lb 等)保留,避免混淆 if unit in (None, "", "MPa"): return val return f"{val}{display_unit}" return f"{val}{display_unit}"
[文档] @lru_cache(maxsize=1) def get_field_formatter() -> TemplateFieldFormatter: """全局单例:懒加载 field_display_formats。""" return TemplateFieldFormatter.load()