"""合格证预览 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