diff --git a/README-EN.md b/README-EN.md index 5ecfc50..aaa322a 100644 --- a/README-EN.md +++ b/README-EN.md @@ -396,10 +396,14 @@ uv venv --python 3.11 # Create Python 3.11 environment brew install pango gdk-pixbuf libffi # 2. Set environment variable (required) +# Apple Silicon export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH +# Intel Mac +export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH # Or permanently add to ~/.zshrc echo 'export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc +# Intel users: echo 'export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc source ~/.zshrc ``` @@ -439,7 +443,18 @@ sudo yum install -y pango gdk-pixbuf2 libffi-devel cairo # Visit: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases # Download the latest .exe file and install -# 2. Restart command line or IDE +# 2. Add the GTK installation bin directory to PATH (open a new terminal afterwards) +# Default path example (replace with your custom install path if different) +set PATH=C:\Program Files\GTK3-Runtime Win64\bin;%PATH% + +# Optional: persist the setting +setx PATH "C:\Program Files\GTK3-Runtime Win64\bin;%PATH%" + +# If installed to a custom path, replace with your actual path, or set GTK_BIN_PATH=, then reopen the terminal + +# 3. Verify in a new terminal +python -m ReportEngine.utils.dependency_check +# You should see “✓ Pango dependency check passed” ``` diff --git a/README.md b/README.md index 8dd846a..b64267a 100644 --- a/README.md +++ b/README.md @@ -389,7 +389,7 @@ conda activate your_conda_name uv venv --python 3.11 # 创建3.11环境 ``` -### 3. 安装 PDF 导出所需系统依赖(可选) +### 2. 安装 PDF 导出所需系统依赖(可选) > ⚠️ **注意**:如果您需要使用 PDF 导出功能,请按照以下步骤安装系统依赖。如果不需要 PDF 导出功能,可以跳过此步骤,系统其他功能不受影响。 @@ -404,10 +404,15 @@ brew install pango gdk-pixbuf libffi # 步骤 2: 设置环境变量(⚠️ 必须执行!) # 方法一:临时设置(仅当前终端会话有效) +# Apple Silicon export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH +# Intel Mac +export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH # 方法二:永久设置(推荐) echo 'export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc +# Intel 用户请改为: +# echo 'export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc source ~/.zshrc ``` @@ -462,7 +467,18 @@ sudo yum install -y pango gdk-pixbuf2 libffi-devel cairo # 访问:https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases # 下载最新版本的 .exe 文件并安装 -# 2. 重启命令行或 IDE +# 2. 将 GTK 安装目录下的 bin 添加到 PATH(安装后请重新打开终端) +# 默认路径示例(如果安装在其他目录,请替换成你的实际路径) +set PATH=C:\Program Files\GTK3-Runtime Win64\bin;%PATH% + +# 可选:永久添加到 PATH +setx PATH "C:\Program Files\GTK3-Runtime Win64\bin;%PATH%" + +# 如果安装在自定义目录,请替换为实际路径,或设置环境变量 GTK_BIN_PATH=你的bin路径,再重新打开终端 + +# 3. 验证(新终端执行) +python -m ReportEngine.utils.dependency_check +# 输出包含 “✓ Pango 依赖检测通过” 表示配置正确 ``` diff --git a/ReportEngine/flask_interface.py b/ReportEngine/flask_interface.py index db28fa2..4e31420 100644 --- a/ReportEngine/flask_interface.py +++ b/ReportEngine/flask_interface.py @@ -1191,8 +1191,8 @@ def export_pdf(task_id: str): return jsonify({ 'success': False, 'error': 'PDF 导出功能不可用:缺少系统依赖', - 'details': '请查看根目录 README.md 第393行「PDF 导出依赖」部分了解如何安装依赖', - 'help_url': 'https://github.com/666ghj/BettaFish#3-安装-pdf-导出所需系统依赖可选', + 'details': '请查看根目录 README.md “源码启动”的第二步(PDF 导出依赖)了解安装方法', + 'help_url': 'https://github.com/666ghj/BettaFish#2-安装-pdf-导出所需系统依赖可选', 'system_message': pango_message }), 503 @@ -1280,8 +1280,8 @@ def export_pdf_from_ir(): return jsonify({ 'success': False, 'error': 'PDF 导出功能不可用:缺少系统依赖', - 'details': '请查看根目录 README.md 第393行「PDF 导出依赖」部分了解如何安装依赖', - 'help_url': 'https://github.com/666ghj/BettaFish#3-安装-pdf-导出所需系统依赖可选', + 'details': '请查看根目录 README.md “源码启动”的第二步(PDF 导出依赖)了解安装方法', + 'help_url': 'https://github.com/666ghj/BettaFish#2-安装-pdf-导出所需系统依赖可选', 'system_message': pango_message }), 503 diff --git a/ReportEngine/renderers/pdf_renderer.py b/ReportEngine/renderers/pdf_renderer.py index b3b08d9..bfadad7 100644 --- a/ReportEngine/renderers/pdf_renderer.py +++ b/ReportEngine/renderers/pdf_renderer.py @@ -13,33 +13,57 @@ from pathlib import Path from typing import Any, Dict from datetime import datetime from loguru import logger +from ReportEngine.utils.dependency_check import ( + prepare_pango_environment, + check_pango_available, +) # 在导入WeasyPrint之前,尝试补充常见的macOS Homebrew动态库路径, # 避免因未设置DYLD_LIBRARY_PATH而找不到pango/cairo等依赖。 if sys.platform == 'darwin': - brew_lib = Path('/opt/homebrew/lib') - if brew_lib.exists(): - current = os.environ.get('DYLD_LIBRARY_PATH', '') - if str(brew_lib) not in current.split(':'): - os.environ['DYLD_LIBRARY_PATH'] = f"{brew_lib}{':' + current if current else ''}" + mac_libs = [Path('/opt/homebrew/lib'), Path('/usr/local/lib')] + current = os.environ.get('DYLD_LIBRARY_PATH', '') + inserts = [] + for lib in mac_libs: + if lib.exists() and str(lib) not in current.split(':'): + inserts.append(str(lib)) + if inserts: + os.environ['DYLD_LIBRARY_PATH'] = ":".join(inserts + ([current] if current else [])) + +# Windows: 自动补充常见 GTK/Pango 运行时路径,避免 DLL 加载失败 +if sys.platform.startswith('win'): + added = prepare_pango_environment() + if added: + logger.debug(f"已自动添加 GTK 运行时路径: {added}") try: from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration WEASYPRINT_AVAILABLE = True + PDF_DEP_STATUS = "OK" except (ImportError, OSError) as e: WEASYPRINT_AVAILABLE = False - # 判断错误类型以提供更友好的提示 + # 判断错误类型以提供更友好的提示,并尝试输出缺失依赖的详细信息 + try: + _, dep_message = check_pango_available() + except Exception: + dep_message = None + if isinstance(e, OSError): - logger.warning( + msg = dep_message or ( "PDF 导出依赖缺失(系统库未安装或环境变量未设置)," "PDF 导出功能将不可用。其他功能不受影响。" ) + logger.warning(msg) + PDF_DEP_STATUS = msg else: - logger.warning("WeasyPrint未安装,PDF导出功能将不可用") + msg = dep_message or "WeasyPrint未安装,PDF导出功能将不可用" + logger.warning(msg) + PDF_DEP_STATUS = msg except Exception as e: WEASYPRINT_AVAILABLE = False - logger.warning(f"WeasyPrint 加载失败: {e},PDF导出功能将不可用") + PDF_DEP_STATUS = f"WeasyPrint 加载失败: {e},PDF导出功能将不可用" + logger.warning(PDF_DEP_STATUS) from .html_renderer import HTMLRenderer from .pdf_layout_optimizer import PDFLayoutOptimizer, PDFLayoutConfig @@ -73,7 +97,11 @@ class PDFRenderer: self.layout_optimizer = layout_optimizer or PDFLayoutOptimizer() if not WEASYPRINT_AVAILABLE: - raise RuntimeError("WeasyPrint未安装,请运行: pip install weasyprint") + raise RuntimeError( + PDF_DEP_STATUS + if 'PDF_DEP_STATUS' in globals() else + "WeasyPrint未安装,请运行: pip install weasyprint" + ) # 初始化图表转换器 try: diff --git a/ReportEngine/utils/dependency_check.py b/ReportEngine/utils/dependency_check.py index 5a3f11c..de31904 100644 --- a/ReportEngine/utils/dependency_check.py +++ b/ReportEngine/utils/dependency_check.py @@ -2,9 +2,12 @@ 检测系统依赖工具 用于检测 PDF 生成所需的系统依赖 """ +import os import sys import platform +from pathlib import Path from loguru import logger +from ctypes import util as ctypes_util def _get_platform_specific_instructions(): @@ -24,10 +27,12 @@ def _get_platform_specific_instructions(): "║ brew install pango gdk-pixbuf libffi ║\n" "║ ║\n" "║ 2. 设置环境变量(重要!): ║\n" - "║ export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH ║\n" + "║ Apple Silicon: export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH ║\n" + "║ Intel Mac: export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH ║\n" "║ ║\n" "║ 3. 永久生效(推荐): ║\n" "║ echo 'export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc ║\n" + "║ 或 echo 'export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc ║\n" "║ source ~/.zshrc ║\n" ) elif system == "Linux": @@ -40,13 +45,24 @@ def _get_platform_specific_instructions(): "║ ║\n" "║ CentOS/RHEL: ║\n" "║ sudo yum install pango gdk-pixbuf2 libffi-devel cairo ║\n" + "║ ║\n" + "║ 若仍提示缺库:export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH ║\n" + "║ sudo ldconfig ║\n" ) elif system == "Windows": return ( "║ 🪟 Windows 系统解决方案: ║\n" "║ ║\n" - "║ 下载并安装 GTK3 Runtime: ║\n" - "║ https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases ║\n" + "║ 1. 安装 GTK3 Runtime: ║\n" + "║ https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases ║\n" + "║ ║\n" + "║ 2. 将 GTK 安装目录下的 bin 加入 PATH(需新开终端): ║\n" + "║ set PATH=C:\\Program Files\\GTK3-Runtime Win64\\bin;%PATH% ║\n" + "║ (若自定义路径,请替换为实际安装路径) ║\n" + "║ ║\n" + "║ 3. 验证:在新终端运行 ║\n" + "║ python -m ReportEngine.utils.dependency_check ║\n" + "║ 看到 ✓ 提示即表示 PDF 导出可用 ║\n" ) else: return ( @@ -54,6 +70,158 @@ def _get_platform_specific_instructions(): ) +def _ensure_windows_gtk_paths(): + """ + 为 Windows 自动补充 GTK/Pango 运行时搜索路径,解决 DLL 未找到问题。 + + Returns: + str | None: 成功添加的路径(没有命中则为 None) + """ + if platform.system() != "Windows": + return None + + candidates = [] + seen = set() + + def _add_candidate(path_like): + if not path_like: + return + p = Path(path_like) + # 如果传入的是安装根目录,尝试拼接 bin + if p.is_dir() and p.name.lower() == "bin": + key = str(p.resolve()).lower() + if key not in seen: + seen.add(key) + candidates.append(p) + else: + for maybe in (p, p / "bin"): + key = str(maybe.resolve()).lower() + if maybe.exists() and key not in seen: + seen.add(key) + candidates.append(maybe) + + # 用户自定义提示优先 + for env_var in ("GTK3_RUNTIME_PATH", "GTK_RUNTIME_PATH", "GTK_BIN_PATH", "GTK_BIN_DIR", "GTK_PATH"): + _add_candidate(os.environ.get(env_var)) + + program_files = os.environ.get("ProgramFiles", r"C:\\Program Files") + program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\\Program Files (x86)") + default_dirs = [ + Path(program_files) / "GTK3-Runtime Win64", + Path(program_files_x86) / "GTK3-Runtime Win64", + Path(program_files) / "GTK3-Runtime Win32", + Path(program_files_x86) / "GTK3-Runtime Win32", + Path(program_files) / "GTK3-Runtime", + Path(program_files_x86) / "GTK3-Runtime", + ] + + # 常见自定义安装位置(其他盘符 / DevelopSoftware 目录) + common_drives = ["C", "D", "E", "F"] + common_names = ["GTK3-Runtime Win64", "GTK3-Runtime Win32", "GTK3-Runtime"] + for drive in common_drives: + root = Path(f"{drive}:/") + for name in common_names: + default_dirs.append(root / name) + default_dirs.append(root / "DevelopSoftware" / name) + + # 扫描 Program Files 下所有以 GTK 开头的目录,适配自定义安装目录名 + for root in (program_files, program_files_x86): + root_path = Path(root) + if root_path.exists(): + for child in root_path.glob("GTK*"): + default_dirs.append(child) + + for d in default_dirs: + _add_candidate(d) + + # 如果用户已把自定义路径加入 PATH,也尝试识别 + path_entries = os.environ.get("PATH", "").split(os.pathsep) + for entry in path_entries: + if not entry: + continue + # 粗筛包含 gtk 或 pango 的目录 + if "gtk" in entry.lower() or "pango" in entry.lower(): + _add_candidate(entry) + + for path in candidates: + if not path or not path.exists(): + continue + if not any(path.glob("pango*-1.0-*.dll")) and not (path / "pango-1.0-0.dll").exists(): + continue + + try: + if hasattr(os, "add_dll_directory"): + os.add_dll_directory(str(path)) + except Exception: + # 如果添加失败,继续尝试 PATH 方式 + pass + + current_path = os.environ.get("PATH", "") + if str(path) not in current_path.split(";"): + os.environ["PATH"] = f"{path};{current_path}" + + return str(path) + + return None + + +def prepare_pango_environment(): + """ + 初始化运行所需的本地依赖搜索路径(当前主要针对 Windows 和 macOS)。 + + Returns: + str | None: 成功添加的路径(没有命中则为 None) + """ + system = platform.system() + if system == "Windows": + return _ensure_windows_gtk_paths() + if system == "Darwin": + # 自动补全 DYLD_LIBRARY_PATH,兼容 Apple Silicon 与 Intel + candidates = [Path("/opt/homebrew/lib"), Path("/usr/local/lib")] + current = os.environ.get("DYLD_LIBRARY_PATH", "") + added = [] + for c in candidates: + if c.exists() and str(c) not in current.split(":"): + added.append(str(c)) + if added: + os.environ["DYLD_LIBRARY_PATH"] = ":".join(added + ([current] if current else [])) + return os.environ["DYLD_LIBRARY_PATH"] + return None + + +def _probe_native_libs(): + """ + 使用 ctypes 查找关键原生库,帮助定位缺失组件。 + + Returns: + list[str]: 未找到的库标识 + """ + system = platform.system() + targets = [] + + if system == "Windows": + targets = [ + ("pango", ["pango-1.0-0"]), + ("gobject", ["gobject-2.0-0"]), + ("gdk-pixbuf", ["gdk_pixbuf-2.0-0"]), + ("cairo", ["cairo-2"]), + ] + else: + targets = [ + ("pango", ["pango-1.0"]), + ("gobject", ["gobject-2.0"]), + ("gdk-pixbuf", ["gdk_pixbuf-2.0"]), + ("cairo", ["cairo", "cairo-2"]), + ] + + missing = [] + for key, variants in targets: + found = any(ctypes_util.find_library(v) for v in variants) + if not found: + missing.append(key) + return missing + + def check_pango_available(): """ 检测 Pango 库是否可用 @@ -61,6 +229,9 @@ def check_pango_available(): Returns: tuple: (is_available: bool, message: str) """ + added_path = prepare_pango_environment() + missing_native = _probe_native_libs() + try: # 尝试导入 weasyprint 并初始化 Pango from weasyprint import HTML @@ -74,6 +245,21 @@ def check_pango_available(): # Pango 库未安装或无法加载 error_msg = str(e) platform_instructions = _get_platform_specific_instructions() + windows_hint = "" + if platform.system() == "Windows": + path_display = added_path or "未找到默认路径" + # 控制长度,避免破坏提示框宽度 + if len(path_display) > 38: + path_display = path_display[:35] + "..." + windows_hint = f"║ 已尝试自动添加 GTK 路径: {path_display:<38}║\n" + arch_note = "║ 🔍 若已安装仍报错:确认 Python/GTK 位数一致,重开终端 ║\n" + else: + arch_note = "" + + missing_note = "" + if missing_native: + missing_str = ", ".join(missing_native) + missing_note = f"║ 未识别到的依赖: {missing_str:<46}║\n" if 'gobject' in error_msg.lower() or 'pango' in error_msg.lower() or 'gdk' in error_msg.lower(): return False, ( @@ -82,12 +268,15 @@ def check_pango_available(): "║ ║\n" "║ 📄 PDF 导出功能将不可用(其他功能不受影响) ║\n" "║ ║\n" + f"{windows_hint}" + f"{arch_note}" + f"{missing_note}" f"{platform_instructions}" "║ ║\n" - "║ 📖 完整文档:根目录 README.md 第393行「PDF 导出依赖」 ║\n" + "║ 📖 完整文档:根目录 README.md ‘源码启动’的第二步 ║\n" "╚════════════════════════════════════════════════════════════════╝" ) - return False, f"⚠ PDF 依赖加载失败: {error_msg}" + return False, f"⚠ PDF 依赖加载失败: {error_msg};缺失/未识别: {', '.join(missing_native) if missing_native else '未知'}" except ImportError as e: # weasyprint 未安装 return False, (