#!/usr/bin/env python # -*- coding: utf-8 -*- """ PDF生成引擎 使用 WeasyPrint 将 HTML 转换为 PDF(对中文字体支持更好) """ import io import json import re from typing import Optional, Dict, Any from datetime import datetime from jinja2 import Environment, BaseLoader, select_autoescape # WeasyPrint 导入 import logging logger = logging.getLogger(__name__) try: from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration WEASYPRINT_AVAILABLE = True except (ImportError, OSError) as e: WEASYPRINT_AVAILABLE = False HTML = None CSS = None FontConfiguration = None logger.warning( f"WeasyPrint 不可用,PDF 生成功能将被禁用。" f"如需使用 PDF 生成功能,请安装系统依赖: brew install pango gdk-pixbuf libffi " f"(错误: {e})" ) # 默认中文字体列表 DEFAULT_CHINESE_FONTS = '"Noto Sans CJK SC", "Noto Serif CJK SC", "WenQuanYi Zen Hei", "WenQuanYi Micro Hei", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "SimSun", "SimHei", "STSong", "STHeiti", "Source Han Sans CN", sans-serif' class TemplateEngine: """模板渲染引擎""" def __init__(self): self.env = Environment( loader=BaseLoader(), autoescape=select_autoescape(['html', 'xml']), ) # 注册自定义过滤器 self.env.filters['date'] = self._format_date self.env.filters['datetime'] = self._format_datetime self.env.filters['money'] = self._format_money self.env.filters['number'] = self._format_number self.env.filters['default'] = self._default_value def _format_date(self, value, format_str: str = "%Y-%m-%d") -> str: """日期格式化""" if not value: return "" if isinstance(value, str): try: value = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError: return value if isinstance(value, datetime): return value.strftime(format_str) return str(value) def _format_datetime(self, value, format_str: str = "%Y-%m-%d %H:%M:%S") -> str: """日期时间格式化""" return self._format_date(value, format_str) def _format_money(self, value, decimal_places: int = 2) -> str: """金额格式化""" if value is None: return "0.00" try: return f"{float(value):,.{decimal_places}f}" except (ValueError, TypeError): return str(value) def _format_number(self, value, decimal_places: int = 0) -> str: """数字格式化""" if value is None: return "0" try: if decimal_places > 0: return f"{float(value):,.{decimal_places}f}" return f"{int(value):,}" except (ValueError, TypeError): return str(value) def _default_value(self, value, default: str = "") -> str: """默认值""" return value if value else default def render_html(self, template_content: str, data: Dict[str, Any]) -> str: """渲染 HTML 模板""" template = self.env.from_string(template_content) return template.render(**data) class DesignerTemplateRenderer: """设计器模板渲染器 - 将设计器 JSON 转换为 HTML""" @classmethod def render(cls, template_json: str, data: Dict[str, Any], css: Optional[str] = None) -> str: """将设计器模板渲染为 HTML""" try: config = json.loads(template_json) if isinstance(template_json, str) else template_json except json.JSONDecodeError: raise ValueError("Invalid template JSON") # 支持两种格式:嵌套的 pageConfig 或扁平的 pageSize/pageOrientation/pageMargin page_config = config.get("pageConfig", {}) if not page_config: page_config = { "size": config.get("pageSize", "A4"), "orientation": config.get("pageOrientation", "portrait"), "margin": config.get("pageMargin", {"top": 20, "right": 20, "bottom": 20, "left": 20}), "customPageWidth": config.get("customPageWidth"), "customPageHeight": config.get("customPageHeight"), } elements = config.get("elements", []) # 构建 HTML html_parts = [cls._build_html_header(page_config, css, config)] html_parts.append('