certflow.views.certificate_preview_painter 源代码

"""合格证预览 QPainter 绘制器(C2:替代 QWebEngineView / HTML 渲染)。

统一打印预览坐标源:均来自 TemplateManager(mm 绝对坐标,与 ESC/P 实际打印一致),
因此空白字段不会造成下方字段上移。字段文本格式化复用 ``TemplateFieldFormatter``
(配置驱动、支持英制 Lb/in,并与 ESC/P 引擎同源),保证所见即所得。

供 ``PrintView``(主预览)与 ``PrintPreviewDialog``(对话框)共用,使单页预览区
收敛为单一 ``QLabel``,整个应用不再依赖 Chromium(消除 offscreen/CI 下 setHtml 段错误)。
"""

from __future__ import annotations

from typing import Any

from PySide6.QtCore import QRect
from PySide6.QtGui import QColor, QFont, QImage, QPainter, QPixmap

from certflow.config.settings import cfg
from certflow.controllers.printer_controller import PrinterController
from certflow.utils.path_utils import get_project_root

# 字段标题(仅 show_labels=True 时显示),与打印/扫描字段一一对应
_FIELD_LABELS: dict[str, str] = {
    "product_name": "产品名称:",
    "product_model": "产品型号:",
    "dn": "公称通径:",
    "pn": "公称压力:",
    "temperature": "工作温度:",
    "medium": "适用介质:",
    "check_standard": "检验标准:",
    "inspector_id": "检验工号:",
    "manufacture_date": "出厂日期:",
    "serial_number": "产品编号:",
    "certificate_no": "合格证号:",
}

# 需经 TemplateFieldFormatter 做模板感知格式化的字段(pn/dn/温度/介质)
_FORMAT_FIELDS = frozenset({"pn", "dn", "temperature", "medium"})


def _draw_background(painter: QPainter, template_key: str, opacity: float, rect: QRect) -> None:
    """叠加底板图(对齐用);文件缺失或为空则跳过并记录日志便于排查。

    补丁37:原实现静默跳过,导致「背景图空白」时无法定位是配置缺失还是路径错。
    现增加 WARNING 日志(输出解析后的绝对路径),并支持 print.yaml 中
    ``background.image_path`` 自定义底板覆盖(留空则用模板默认底板)。
    """
    from certflow.utils.logger import logger

    tmpl = PrinterController.get_template_manager().get_template(template_key)
    bg_path = (tmpl or {}).get("background", "") if tmpl else ""
    # 支持 print.yaml background.image_path 自定义底板覆盖
    custom = cfg("background.image_path", "") or ""
    if custom:
        bg_path = custom
    if not bg_path:
        logger.warning(f"[Preview] 模板 {template_key} 未配置 background,跳过底板(背景空白)")
        return
    img_path = get_project_root() / bg_path
    if not img_path.exists():
        logger.warning(f"[Preview] 底板文件缺失: {img_path} (template={template_key})")
        return
    img = QImage(str(img_path))
    if img.isNull():
        logger.warning(f"[Preview] 底板图片加载失败(空图像): {img_path}")
        return
    painter.save()
    try:
        painter.setOpacity(max(0.0, min(1.0, opacity)))
        painter.drawImage(rect, img)
    finally:
        painter.restore()


def _draw_fields(
    painter: QPainter,
    template_key: str,
    data: dict[str, Any],
    serial_number: str,
    show_labels: bool,
    scale: float,
    label_x_mm: float,
) -> None:
    """按模板坐标绘字段(标题 + 值),空白字段跳过。"""
    tmpl = PrinterController.get_template_manager().get_template(template_key)
    if not tmpl:
        return
    family = PrinterController.family_of(template_key)
    formatter = PrinterController.get_field_formatter()
    font = QFont("SimSun, Microsoft YaHei, sans-serif")
    label_x_px = round(label_x_mm * scale)

    for field_name, pos in tmpl["fields"].items():
        value = data.get(field_name, "")
        if field_name == "serial_number":
            value = serial_number
        value = "" if value is None else str(value)
        if field_name in _FORMAT_FIELDS:
            value, _xoff = formatter.format(field_name, value, family)
        if not value.strip():
            continue

        x_px = round(float(pos.get("x_mm", 0)) * scale)
        y_px = round(float(pos.get("y_mm", 0)) * scale)
        font.setPointSize(int(pos.get("font_size", 9)))
        painter.setFont(font)
        # QPainter.drawText 的 y 为 baseline;HTML top 为盒顶,加 ascent 近似对齐。
        baseline_y = y_px + painter.fontMetrics().ascent()

        if show_labels:
            label = _FIELD_LABELS.get(field_name, "")
            if label:
                painter.setPen(QColor(85, 85, 85))
                painter.drawText(label_x_px, baseline_y, label)
        painter.setPen(QColor(0, 0, 0))
        painter.drawText(x_px, baseline_y, value)


[文档] def render_certificate_pixmap( data: dict[str, Any], serial_number: str, template_key: str, *, show_labels: bool = True, paper_w_mm: float = 60.0, paper_h_mm: float = 100.0, preview_w_px: int = 280, label_x_mm: float = 8.0, bg_show: bool | None = None, bg_opacity: float | None = None, ) -> QPixmap: """按模板坐标用 QPainter 绘制合格证预览位图。 Args: data: 字段原始值字典(field_name → 值);未列出的字段跳过。 serial_number: 当前产品编号(覆盖 data 中的 serial_number 字段)。 template_key: TemplateManager 模板键(决定坐标与语言族)。 show_labels: 是否绘制字段标题。 paper_w_mm / paper_h_mm: 纸张尺寸(mm),用于 mm→px 缩放。 preview_w_px: 预览位图宽度(px),高度按纸张比例推导。 label_x_mm: 标题列 x 坐标(mm)。 bg_show / bg_opacity: 底板叠加开关与透明度;None 时读 ``background`` 配置(默认显示 / 0.3,见 config/print.yaml)。 Returns: QPixmap 预览位图(白底)。 """ scale = (preview_w_px / paper_w_mm) if paper_w_mm else 1.0 h_px = max(1, round(paper_h_mm * scale)) pixmap = QPixmap(preview_w_px, h_px) pixmap.fill(QColor(255, 255, 255)) if bg_show is None: bg_show = bool(cfg("background.show_in_preview", True)) if bg_opacity is None: bg_opacity = float(cfg("background.opacity", 0.3)) painter = QPainter(pixmap) try: painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) if bg_show: _draw_background(painter, template_key, bg_opacity, pixmap.rect()) _draw_fields(painter, template_key, data, serial_number, show_labels, scale, label_x_mm) finally: painter.end() return pixmap