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