Fix the Issue Where Dependencies for PDF Generation are Installed but not Recognized by the Program (Perhaps?)

This commit is contained in:
马一丁
2025-11-22 02:31:35 +08:00
parent a22be6d7dd
commit 37dc8e0a5d
5 changed files with 270 additions and 22 deletions
+16 -1
View File
@@ -396,10 +396,14 @@ uv venv --python 3.11 # Create Python 3.11 environment
brew install pango gdk-pixbuf libffi brew install pango gdk-pixbuf libffi
# 2. Set environment variable (required) # 2. Set environment variable (required)
# Apple Silicon
export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH 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 # Or permanently add to ~/.zshrc
echo 'export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH' >> ~/.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 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 # Visit: https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases
# Download the latest .exe file and install # 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=<your-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”
``` ```
</details> </details>
+18 -2
View File
@@ -389,7 +389,7 @@ conda activate your_conda_name
uv venv --python 3.11 # 创建3.11环境 uv venv --python 3.11 # 创建3.11环境
``` ```
### 3. 安装 PDF 导出所需系统依赖(可选) ### 2. 安装 PDF 导出所需系统依赖(可选)
> ⚠️ **注意**:如果您需要使用 PDF 导出功能,请按照以下步骤安装系统依赖。如果不需要 PDF 导出功能,可以跳过此步骤,系统其他功能不受影响。 > ⚠️ **注意**:如果您需要使用 PDF 导出功能,请按照以下步骤安装系统依赖。如果不需要 PDF 导出功能,可以跳过此步骤,系统其他功能不受影响。
@@ -404,10 +404,15 @@ brew install pango gdk-pixbuf libffi
# 步骤 2: 设置环境变量(⚠️ 必须执行!) # 步骤 2: 设置环境变量(⚠️ 必须执行!)
# 方法一:临时设置(仅当前终端会话有效) # 方法一:临时设置(仅当前终端会话有效)
# Apple Silicon
export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH 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 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 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 # 访问:https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases
# 下载最新版本的 .exe 文件并安装 # 下载最新版本的 .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 依赖检测通过” 表示配置正确
``` ```
</details> </details>
+4 -4
View File
@@ -1191,8 +1191,8 @@ def export_pdf(task_id: str):
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'PDF 导出功能不可用:缺少系统依赖', 'error': 'PDF 导出功能不可用:缺少系统依赖',
'details': '请查看根目录 README.md 第393行「PDF 导出依赖」部分了解如何安装依赖', 'details': '请查看根目录 README.md “源码启动”的第二步(PDF 导出依赖)了解安装方法',
'help_url': 'https://github.com/666ghj/BettaFish#3-安装-pdf-导出所需系统依赖可选', 'help_url': 'https://github.com/666ghj/BettaFish#2-安装-pdf-导出所需系统依赖可选',
'system_message': pango_message 'system_message': pango_message
}), 503 }), 503
@@ -1280,8 +1280,8 @@ def export_pdf_from_ir():
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'PDF 导出功能不可用:缺少系统依赖', 'error': 'PDF 导出功能不可用:缺少系统依赖',
'details': '请查看根目录 README.md 第393行「PDF 导出依赖」部分了解如何安装依赖', 'details': '请查看根目录 README.md “源码启动”的第二步(PDF 导出依赖)了解安装方法',
'help_url': 'https://github.com/666ghj/BettaFish#3-安装-pdf-导出所需系统依赖可选', 'help_url': 'https://github.com/666ghj/BettaFish#2-安装-pdf-导出所需系统依赖可选',
'system_message': pango_message 'system_message': pango_message
}), 503 }), 503
+38 -10
View File
@@ -13,33 +13,57 @@ from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from datetime import datetime from datetime import datetime
from loguru import logger from loguru import logger
from ReportEngine.utils.dependency_check import (
prepare_pango_environment,
check_pango_available,
)
# 在导入WeasyPrint之前,尝试补充常见的macOS Homebrew动态库路径, # 在导入WeasyPrint之前,尝试补充常见的macOS Homebrew动态库路径,
# 避免因未设置DYLD_LIBRARY_PATH而找不到pango/cairo等依赖。 # 避免因未设置DYLD_LIBRARY_PATH而找不到pango/cairo等依赖。
if sys.platform == 'darwin': if sys.platform == 'darwin':
brew_lib = Path('/opt/homebrew/lib') mac_libs = [Path('/opt/homebrew/lib'), Path('/usr/local/lib')]
if brew_lib.exists(): current = os.environ.get('DYLD_LIBRARY_PATH', '')
current = os.environ.get('DYLD_LIBRARY_PATH', '') inserts = []
if str(brew_lib) not in current.split(':'): for lib in mac_libs:
os.environ['DYLD_LIBRARY_PATH'] = f"{brew_lib}{':' + current if current else ''}" 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: try:
from weasyprint import HTML, CSS from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration from weasyprint.text.fonts import FontConfiguration
WEASYPRINT_AVAILABLE = True WEASYPRINT_AVAILABLE = True
PDF_DEP_STATUS = "OK"
except (ImportError, OSError) as e: except (ImportError, OSError) as e:
WEASYPRINT_AVAILABLE = False WEASYPRINT_AVAILABLE = False
# 判断错误类型以提供更友好的提示 # 判断错误类型以提供更友好的提示,并尝试输出缺失依赖的详细信息
try:
_, dep_message = check_pango_available()
except Exception:
dep_message = None
if isinstance(e, OSError): if isinstance(e, OSError):
logger.warning( msg = dep_message or (
"PDF 导出依赖缺失(系统库未安装或环境变量未设置)," "PDF 导出依赖缺失(系统库未安装或环境变量未设置),"
"PDF 导出功能将不可用。其他功能不受影响。" "PDF 导出功能将不可用。其他功能不受影响。"
) )
logger.warning(msg)
PDF_DEP_STATUS = msg
else: else:
logger.warning("WeasyPrint未安装,PDF导出功能将不可用") msg = dep_message or "WeasyPrint未安装,PDF导出功能将不可用"
logger.warning(msg)
PDF_DEP_STATUS = msg
except Exception as e: except Exception as e:
WEASYPRINT_AVAILABLE = False 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 .html_renderer import HTMLRenderer
from .pdf_layout_optimizer import PDFLayoutOptimizer, PDFLayoutConfig from .pdf_layout_optimizer import PDFLayoutOptimizer, PDFLayoutConfig
@@ -73,7 +97,11 @@ class PDFRenderer:
self.layout_optimizer = layout_optimizer or PDFLayoutOptimizer() self.layout_optimizer = layout_optimizer or PDFLayoutOptimizer()
if not WEASYPRINT_AVAILABLE: 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: try:
+194 -5
View File
@@ -2,9 +2,12 @@
检测系统依赖工具 检测系统依赖工具
用于检测 PDF 生成所需的系统依赖 用于检测 PDF 生成所需的系统依赖
""" """
import os
import sys import sys
import platform import platform
from pathlib import Path
from loguru import logger from loguru import logger
from ctypes import util as ctypes_util
def _get_platform_specific_instructions(): def _get_platform_specific_instructions():
@@ -24,10 +27,12 @@ def _get_platform_specific_instructions():
"║ brew install pango gdk-pixbuf libffi ║\n" "║ brew install pango gdk-pixbuf libffi ║\n"
"║ ║\n" "║ ║\n"
"║ 2. 设置环境变量(重要!): ║\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" "║ ║\n"
"║ 3. 永久生效(推荐): ║\n" "║ 3. 永久生效(推荐): ║\n"
"║ echo 'export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc ║\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" "║ source ~/.zshrc ║\n"
) )
elif system == "Linux": elif system == "Linux":
@@ -40,13 +45,24 @@ def _get_platform_specific_instructions():
"║ ║\n" "║ ║\n"
"║ CentOS/RHEL: ║\n" "║ CentOS/RHEL: ║\n"
"║ sudo yum install pango gdk-pixbuf2 libffi-devel cairo ║\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": elif system == "Windows":
return ( return (
"║ 🪟 Windows 系统解决方案: ║\n" "║ 🪟 Windows 系统解决方案: ║\n"
"║ ║\n" "║ ║\n"
"下载并安装 GTK3 Runtime\n" "1. 安装 GTK3 Runtime \n"
"║ https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases ║\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: else:
return ( 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(): def check_pango_available():
""" """
检测 Pango 库是否可用 检测 Pango 库是否可用
@@ -61,6 +229,9 @@ def check_pango_available():
Returns: Returns:
tuple: (is_available: bool, message: str) tuple: (is_available: bool, message: str)
""" """
added_path = prepare_pango_environment()
missing_native = _probe_native_libs()
try: try:
# 尝试导入 weasyprint 并初始化 Pango # 尝试导入 weasyprint 并初始化 Pango
from weasyprint import HTML from weasyprint import HTML
@@ -74,6 +245,21 @@ def check_pango_available():
# Pango 库未安装或无法加载 # Pango 库未安装或无法加载
error_msg = str(e) error_msg = str(e)
platform_instructions = _get_platform_specific_instructions() 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(): if 'gobject' in error_msg.lower() or 'pango' in error_msg.lower() or 'gdk' in error_msg.lower():
return False, ( return False, (
@@ -82,12 +268,15 @@ def check_pango_available():
"║ ║\n" "║ ║\n"
"║ 📄 PDF 导出功能将不可用(其他功能不受影响) ║\n" "║ 📄 PDF 导出功能将不可用(其他功能不受影响) ║\n"
"║ ║\n" "║ ║\n"
f"{windows_hint}"
f"{arch_note}"
f"{missing_note}"
f"{platform_instructions}" f"{platform_instructions}"
"║ ║\n" "║ ║\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: except ImportError as e:
# weasyprint 未安装 # weasyprint 未安装
return False, ( return False, (