Files
tyapi-server/internal/shared/pdf/font_manager.go
2025-12-04 16:17:27 +08:00

313 lines
9.0 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"
"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
}
// NewFontManager 创建字体管理器
func NewFontManager(logger *zap.Logger) *FontManager {
return &FontManager{
logger: logger,
chineseFontName: "ChineseFont",
watermarkFontName: "WatermarkFont",
}
}
// LoadChineseFont 加载中文字体到PDF
func (fm *FontManager) LoadChineseFont(pdf *gofpdf.Fpdf) bool {
if fm.chineseFontLoaded {
return true
}
fontPaths := fm.getChineseFontPaths()
if len(fontPaths) == 0 {
// 字体文件不存在,使用系统默认字体,不记录警告
return false
}
// 尝试加载字体
for _, fontPath := range fontPaths {
if fm.tryAddFont(pdf, fontPath, fm.chineseFontName) {
fm.chineseFontLoaded = true
return true
}
}
// 无法加载字体,使用系统默认字体,不记录警告
return false
}
// LoadWatermarkFont 加载水印字体到PDF
func (fm *FontManager) LoadWatermarkFont(pdf *gofpdf.Fpdf) bool {
if fm.watermarkFontLoaded {
return true
}
fontPaths := fm.getWatermarkFontPaths()
if len(fontPaths) == 0 {
return false
}
// 尝试加载字体
for _, fontPath := range fontPaths {
if fm.tryAddFont(pdf, fontPath, fm.watermarkFontName) {
fm.watermarkFontLoaded = true
return true
}
}
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.String("font_path", fontPath),
zap.String("font_name", fontName),
zap.Any("panic_value", r),
)
}
}()
// 确保路径是绝对路径
absFontPath, err := filepath.Abs(fontPath)
if err != nil {
fm.logger.Warn("无法获取字体文件绝对路径",
zap.String("font_path", fontPath),
zap.Error(err),
)
absFontPath = fontPath
}
fm.logger.Debug("尝试添加字体",
zap.String("font_path", absFontPath),
zap.String("font_name", fontName),
)
// 检查文件是否存在
fileInfo, err := os.Stat(absFontPath)
if err != nil {
fm.logger.Debug("字体文件不存在",
zap.String("font_path", absFontPath),
zap.Error(err),
)
return false
}
if !fileInfo.Mode().IsRegular() {
fm.logger.Warn("字体路径不是普通文件",
zap.String("font_path", absFontPath),
)
return false
}
// 确保路径是绝对路径gofpdf在Output时需要绝对路径
// 首先确保absFontPath是绝对路径
if !filepath.IsAbs(absFontPath) {
fm.logger.Warn("字体路径不是绝对路径,重新转换",
zap.String("original_path", absFontPath),
)
if newAbsPath, err := filepath.Abs(absFontPath); err == nil {
absFontPath = newAbsPath
}
}
// 使用filepath.ToSlash统一路径分隔符Linux下使用/
// 注意ToSlash不会改变路径的绝对/相对性质,只统一分隔符
normalizedPath := filepath.ToSlash(absFontPath)
// 在Linux下绝对路径必须以/开头
// 如果normalizedPath不是以/开头,说明转换有问题
if len(normalizedPath) == 0 || normalizedPath[0] != '/' {
fm.logger.Error("字体路径转换后不是绝对路径(不以/开头)",
zap.String("abs_font_path", absFontPath),
zap.String("normalized_path", normalizedPath),
)
// 重新转换为绝对路径
if newAbsPath, err := filepath.Abs(absFontPath); err == nil {
absFontPath = newAbsPath
normalizedPath = filepath.ToSlash(newAbsPath)
fm.logger.Info("重新转换后的路径",
zap.String("new_normalized_path", normalizedPath),
)
}
}
fm.logger.Debug("准备添加字体到gofpdf",
zap.String("original_path", fontPath),
zap.String("abs_path", absFontPath),
zap.String("normalized_path", normalizedPath),
zap.String("font_name", fontName),
)
// gofpdf v2使用AddUTF8Font添加支持UTF-8的字体
// 注意gofpdf在Output时可能会重新解析路径必须确保路径格式正确
// 关键:确保路径是绝对路径且以/开头并使用filepath.ToSlash统一分隔符
// 如果normalizedPath不是以/开头,说明路径有问题,需要重新处理
if len(normalizedPath) == 0 || normalizedPath[0] != '/' {
fm.logger.Error("字体路径格式错误无法添加到PDF",
zap.String("normalized_path", normalizedPath),
zap.String("font_name", fontName),
)
return false
}
// 记录传递给gofpdf的实际路径
fm.logger.Info("添加字体到gofpdf",
zap.String("font_path", normalizedPath),
zap.String("font_name", fontName),
zap.Bool("is_absolute", len(normalizedPath) > 0 && normalizedPath[0] == '/'),
)
pdf.AddUTF8Font(fontName, "", normalizedPath) // 常规样式
pdf.AddUTF8Font(fontName, "B", normalizedPath) // 粗体样式
// 验证字体是否可用
// 注意gofpdf可能在AddUTF8Font时不会立即加载字体而是在Output时才加载
// 所以这里验证可能失败,但不一定代表字体无法使用
pdf.SetFont(fontName, "", 12)
testWidth := pdf.GetStringWidth("测试")
if testWidth == 0 {
fm.logger.Warn("字体添加后验证失败测试文本宽度为0但会在Output时重新尝试",
zap.String("font_path", normalizedPath),
zap.String("font_name", fontName),
)
// 注意即使验证失败也返回true因为gofpdf在Output时才会真正加载字体文件
// 这里的验证可能不准确
}
fm.logger.Info("字体已添加到PDF将在Output时加载",
zap.String("font_path", normalizedPath),
zap.String("font_name", fontName),
zap.Float64("test_width", testWidth),
)
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 构建字体文件路径列表仅从resources/pdf/fonts加载
// 返回所有存在的字体文件的绝对路径
func (fm *FontManager) buildFontPaths(fontNames []string) []string {
// 获取resources/pdf目录已返回绝对路径
resourcesPDFDir := GetResourcesPDFDir()
if resourcesPDFDir == "" {
fm.logger.Error("无法获取resources/pdf目录路径")
return []string{}
}
// 构建字体目录路径resourcesPDFDir已经是绝对路径
fontsDir := filepath.Join(resourcesPDFDir, "fonts")
fm.logger.Debug("查找字体文件",
zap.String("resources_pdf_dir", resourcesPDFDir),
zap.String("fonts_dir", fontsDir),
zap.Strings("font_names", fontNames),
)
// 构建字体文件路径列表(都是绝对路径)
var fontPaths []string
for _, fontName := range fontNames {
fontPath := filepath.Join(fontsDir, fontName)
// 确保是绝对路径
if absPath, err := filepath.Abs(fontPath); err == nil {
fontPaths = append(fontPaths, absPath)
} else {
fm.logger.Warn("无法获取字体文件绝对路径",
zap.String("font_path", fontPath),
zap.Error(err),
)
}
}
// 过滤出实际存在的字体文件
var existingFonts []string
for _, fontPath := range fontPaths {
if info, err := os.Stat(fontPath); err == nil && info.Mode().IsRegular() {
existingFonts = append(existingFonts, fontPath)
fm.logger.Debug("找到字体文件", zap.String("font_path", fontPath))
} else {
fm.logger.Debug("字体文件不存在",
zap.String("font_path", fontPath),
zap.Error(err),
)
}
}
if len(existingFonts) == 0 {
fm.logger.Warn("未找到任何字体文件",
zap.String("fonts_dir", fontsDir),
zap.Strings("attempted_fonts", fontPaths),
)
} else {
fm.logger.Info("找到字体文件",
zap.Int("count", len(existingFonts)),
zap.Strings("font_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
}