Files
tyapi-server/internal/shared/pdf/font_manager.go
2025-12-03 18:25:04 +08:00

311 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pdf
import (
"os"
"path/filepath"
"runtime"
"github.com/jung-kurt/gofpdf/v2"
"go.uber.org/zap"
)
// FontManager 字体管理器
type FontManager struct {
logger *zap.Logger
chineseFontName string
chineseFontLoaded bool
watermarkFontName string
watermarkFontLoaded bool
baseDir string // 缓存基础目录,避免重复计算
}
// NewFontManager 创建字体管理器
func NewFontManager(logger *zap.Logger) *FontManager {
// 获取当前文件所在目录
_, filename, _, _ := runtime.Caller(0)
baseDir := filepath.Dir(filename)
fm := &FontManager{
logger: logger,
chineseFontName: "ChineseFont",
watermarkFontName: "WatermarkFont",
baseDir: baseDir,
}
// 记录基础目录(用于调试)
logger.Debug("字体管理器初始化",
zap.String("base_dir", baseDir),
zap.String("caller_file", filename))
return fm
}
// LoadChineseFont 加载中文字体到PDF
func (fm *FontManager) LoadChineseFont(pdf *gofpdf.Fpdf) bool {
if fm.chineseFontLoaded {
return true
}
fontPaths := fm.getChineseFontPaths()
if len(fontPaths) == 0 {
fm.logger.Warn("未找到中文字体文件PDF中的中文可能显示为空白",
zap.String("base_dir", fm.baseDir),
zap.String("hint", "请确保字体文件存在于 internal/shared/pdf/fonts/ 目录下,或设置 PDF_FONT_DIR 环境变量"))
return false
}
// 尝试加载字体
for _, fontPath := range fontPaths {
fm.logger.Debug("尝试加载字体", zap.String("font_path", fontPath))
if fm.tryAddFont(pdf, fontPath, fm.chineseFontName) {
fm.chineseFontLoaded = true
fm.logger.Info("成功加载中文字体", zap.String("font_path", fontPath))
return true
} else {
fm.logger.Debug("字体加载失败", zap.String("font_path", fontPath))
}
}
fm.logger.Warn("所有中文字体文件都无法加载PDF中的中文可能显示为空白",
zap.Int("tried_paths", len(fontPaths)),
zap.Strings("font_paths", fontPaths))
return false
}
// LoadWatermarkFont 加载水印字体到PDF
func (fm *FontManager) LoadWatermarkFont(pdf *gofpdf.Fpdf) bool {
if fm.watermarkFontLoaded {
return true
}
fontPaths := fm.getWatermarkFontPaths()
if len(fontPaths) == 0 {
fm.logger.Warn("未找到水印字体文件,将使用主字体")
return false
}
// 尝试加载字体
for _, fontPath := range fontPaths {
if fm.tryAddFont(pdf, fontPath, fm.watermarkFontName) {
fm.watermarkFontLoaded = true
fm.logger.Info("成功加载水印字体", zap.String("font_path", fontPath))
return true
}
}
fm.logger.Warn("所有水印字体文件都无法加载,将使用主字体")
return false
}
// tryAddFont 尝试添加字体(统一处理中文字体和水印字体)
func (fm *FontManager) tryAddFont(pdf *gofpdf.Fpdf, fontPath, fontName string) bool {
defer func() {
if r := recover(); r != nil {
fm.logger.Error("添加字体时发生panic",
zap.Any("panic", r),
zap.String("font_path", fontPath),
zap.String("font_name", fontName))
}
}()
// 检查文件是否存在
if _, err := os.Stat(fontPath); err != nil {
return false
}
// gofpdf v2使用AddUTF8Font添加支持UTF-8的字体
pdf.AddUTF8Font(fontName, "", fontPath) // 常规样式
pdf.AddUTF8Font(fontName, "B", fontPath) // 粗体样式
// 验证字体是否可用
pdf.SetFont(fontName, "", 12)
testWidth := pdf.GetStringWidth("测试")
if testWidth == 0 {
fm.logger.Debug("字体加载后无法使用",
zap.String("font_path", fontPath),
zap.String("font_name", fontName),
zap.Float64("test_width", testWidth))
return false
}
return true
}
// getChineseFontPaths 获取中文字体路径列表仅TTF格式
func (fm *FontManager) getChineseFontPaths() []string {
// 按优先级排序的字体文件列表
fontNames := []string{
"simhei.ttf", // 黑体(默认)
"simkai.ttf", // 楷体(备选)
"simfang.ttf", // 仿宋(备选)
}
return fm.buildFontPaths(fontNames)
}
// getWatermarkFontPaths 获取水印字体路径列表仅TTF格式
func (fm *FontManager) getWatermarkFontPaths() []string {
// 水印字体文件名(支持大小写变体)
fontNames := []string{
"yunfengfeiyunti-2.ttf",
"YunFengFeiYunTi-2.ttf", // 大写版本(兼容)
}
return fm.buildFontPaths(fontNames)
}
// buildFontPaths 构建字体文件路径列表(支持多种路径查找方式)
func (fm *FontManager) buildFontPaths(fontNames []string) []string {
var fontPaths []string
// 方式1: 使用 pdf/fonts/ 目录(相对于当前文件)
for _, fontName := range fontNames {
fontPaths = append(fontPaths, filepath.Join(fm.baseDir, "fonts", fontName))
}
// 方式2: 尝试相对于工作目录的路径(适用于编译后的二进制文件)
// 这是最常用的方式,适用于直接运行二进制文件的情况
if workDir, err := os.Getwd(); err == nil {
fm.logger.Debug("工作目录", zap.String("work_dir", workDir))
for _, fontName := range fontNames {
// 尝试多个可能的相对路径
paths := []string{
filepath.Join(workDir, "internal", "shared", "pdf", "fonts", fontName),
filepath.Join(workDir, "fonts", fontName),
filepath.Join(workDir, fontName),
// 如果工作目录是项目根目录的父目录
filepath.Join(workDir, "tyapi-server", "internal", "shared", "pdf", "fonts", fontName),
}
fontPaths = append(fontPaths, paths...)
}
} else {
fm.logger.Warn("无法获取工作目录", zap.Error(err))
}
// 方式3: 尝试使用可执行文件所在目录
// 适用于systemd服务或直接运行二进制文件的情况
if execPath, err := os.Executable(); err == nil {
execDir := filepath.Dir(execPath)
// 解析符号链接获取真实路径Linux上很重要
if realPath, err := filepath.EvalSymlinks(execPath); err == nil {
execDir = filepath.Dir(realPath)
}
fm.logger.Debug("可执行文件目录",
zap.String("exec_dir", execDir),
zap.String("exec_path", execPath))
for _, fontName := range fontNames {
paths := []string{
filepath.Join(execDir, "internal", "shared", "pdf", "fonts", fontName),
filepath.Join(execDir, "fonts", fontName),
filepath.Join(execDir, fontName),
// 如果可执行文件在bin目录尝试上一级目录
filepath.Join(filepath.Dir(execDir), "internal", "shared", "pdf", "fonts", fontName),
}
fontPaths = append(fontPaths, paths...)
}
} else {
fm.logger.Warn("无法获取可执行文件路径", zap.Error(err))
}
// 方式4: 尝试使用环境变量指定的路径(如果设置了)
if fontDir := os.Getenv("PDF_FONT_DIR"); fontDir != "" {
fm.logger.Info("使用环境变量指定的字体目录", zap.String("font_dir", fontDir))
for _, fontName := range fontNames {
fontPaths = append(fontPaths, filepath.Join(fontDir, fontName))
}
}
// 方式5: 尝试常见的服务器部署路径(硬编码路径,作为后备方案)
// 支持多种Linux服务器部署场景
commonServerPaths := []string{
"/www/tyapi-server/internal/shared/pdf/fonts", // 用户提供的路径
"/app/internal/shared/pdf/fonts", // Docker常见路径
"/usr/local/tyapi-server/internal/shared/pdf/fonts", // 标准安装路径
"/opt/tyapi-server/internal/shared/pdf/fonts", // 可选安装路径
"/home/ubuntu/tyapi-server/internal/shared/pdf/fonts", // Ubuntu用户目录
"/root/tyapi-server/internal/shared/pdf/fonts", // root用户目录
"/var/www/tyapi-server/internal/shared/pdf/fonts", // Web服务器常见路径
}
for _, basePath := range commonServerPaths {
for _, fontName := range fontNames {
fontPaths = append(fontPaths, filepath.Join(basePath, fontName))
}
}
// 记录所有尝试的路径(用于调试)
fm.logger.Debug("查找字体文件",
zap.Strings("font_names", fontNames),
zap.Int("total_paths", len(fontPaths)),
zap.Strings("paths", fontPaths))
// 过滤出实际存在的字体文件(并检查权限)
var existingFonts []string
for _, fontPath := range fontPaths {
// 检查文件是否存在
fileInfo, err := os.Stat(fontPath)
if err == nil {
// 检查是否为常规文件(不是目录)
if !fileInfo.Mode().IsRegular() {
fm.logger.Debug("路径不是常规文件",
zap.String("font_path", fontPath),
zap.String("mode", fileInfo.Mode().String()))
continue
}
// 检查是否有读取权限Linux上很重要
if fileInfo.Mode().Perm()&0444 == 0 {
fm.logger.Warn("字体文件无读取权限",
zap.String("font_path", fontPath),
zap.String("mode", fileInfo.Mode().String()))
// 仍然尝试添加,因为可能通过其他方式有权限
}
existingFonts = append(existingFonts, fontPath)
fm.logger.Debug("找到字体文件",
zap.String("font_path", fontPath),
zap.Int64("size", fileInfo.Size()))
} else {
// 只在Debug级别记录避免日志过多
fm.logger.Debug("字体文件不存在",
zap.String("font_path", fontPath),
zap.Error(err))
}
}
if len(existingFonts) == 0 {
fm.logger.Warn("未找到任何字体文件",
zap.Strings("font_names", fontNames),
zap.String("base_dir", fm.baseDir),
zap.Int("total_paths_checked", len(fontPaths)))
} else {
fm.logger.Info("找到字体文件",
zap.Int("count", len(existingFonts)),
zap.Strings("paths", existingFonts))
}
return existingFonts
}
// SetFont 设置中文字体
func (fm *FontManager) SetFont(pdf *gofpdf.Fpdf, style string, size float64) {
if fm.chineseFontLoaded {
pdf.SetFont(fm.chineseFontName, style, size)
} else {
// 如果没有中文字体使用Arial作为后备
pdf.SetFont("Arial", style, size)
}
}
// SetWatermarkFont 设置水印字体
func (fm *FontManager) SetWatermarkFont(pdf *gofpdf.Fpdf, style string, size float64) {
if fm.watermarkFontLoaded {
pdf.SetFont(fm.watermarkFontName, style, size)
} else {
// 如果水印字体不可用,使用主字体作为后备
fm.SetFont(pdf, style, size)
}
}
// IsChineseFontAvailable 检查中文字体是否可用
func (fm *FontManager) IsChineseFontAvailable() bool {
return fm.chineseFontLoaded
}