Files
tyapi-server/internal/shared/pdf/page_builder.go

831 lines
27 KiB
Go
Raw Normal View History

2025-12-03 12:03:42 +08:00
package pdf
import (
"context"
"fmt"
"math"
"os"
"strings"
"tyapi-server/internal/domains/product/entities"
"github.com/jung-kurt/gofpdf/v2"
"go.uber.org/zap"
)
// PageBuilder 页面构建器
type PageBuilder struct {
logger *zap.Logger
fontManager *FontManager
textProcessor *TextProcessor
markdownProc *MarkdownProcessor
markdownConverter *MarkdownConverter
tableParser *TableParser
tableRenderer *TableRenderer
jsonProcessor *JSONProcessor
logoPath string
watermarkText string
}
// NewPageBuilder 创建页面构建器
func NewPageBuilder(
logger *zap.Logger,
fontManager *FontManager,
textProcessor *TextProcessor,
markdownProc *MarkdownProcessor,
tableParser *TableParser,
tableRenderer *TableRenderer,
jsonProcessor *JSONProcessor,
logoPath string,
watermarkText string,
) *PageBuilder {
markdownConverter := NewMarkdownConverter(textProcessor)
return &PageBuilder{
logger: logger,
fontManager: fontManager,
textProcessor: textProcessor,
markdownProc: markdownProc,
markdownConverter: markdownConverter,
tableParser: tableParser,
tableRenderer: tableRenderer,
jsonProcessor: jsonProcessor,
logoPath: logoPath,
watermarkText: watermarkText,
}
}
// AddFirstPage 添加第一页(封面页 - 产品功能简述)
func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
pdf.AddPage()
// 添加页眉logo和文字
pb.addHeader(pdf, chineseFontAvailable)
// 添加水印
pb.addWatermark(pdf, chineseFontAvailable)
// 封面页布局 - 居中显示
pageWidth, pageHeight := pdf.GetPageSize()
// 标题区域(页面中上部)
pdf.SetY(80)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "B", 32)
_, lineHt := pdf.GetFontSize()
// 清理产品名称中的无效字符
cleanName := pb.textProcessor.CleanText(product.Name)
pdf.CellFormat(0, lineHt*1.5, cleanName, "", 1, "C", false, 0, "")
// 添加"接口文档"副标题
pdf.Ln(10)
pb.fontManager.SetFont(pdf, "", 18)
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "接口文档", "", 1, "C", false, 0, "")
// 分隔线
pdf.Ln(20)
pdf.SetLineWidth(0.5)
pdf.Line(pageWidth*0.2, pdf.GetY(), pageWidth*0.8, pdf.GetY())
// 产品编码(居中)
pdf.Ln(30)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 14)
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, fmt.Sprintf("产品编码:%s", product.Code), "", 1, "C", false, 0, "")
// 产品描述(居中显示,段落格式)
if product.Description != "" {
pdf.Ln(25)
desc := pb.textProcessor.StripHTML(product.Description)
desc = pb.textProcessor.CleanText(desc)
pb.fontManager.SetFont(pdf, "", 14)
_, lineHt = pdf.GetFontSize()
// 居中对齐的MultiCell通过计算宽度实现
descWidth := pageWidth * 0.7
2025-12-03 16:53:31 +08:00
descLines := pb.safeSplitText(pdf, desc, descWidth, chineseFontAvailable)
2025-12-03 12:03:42 +08:00
currentX := (pageWidth - descWidth) / 2
for _, line := range descLines {
pdf.SetX(currentX)
pdf.CellFormat(descWidth, lineHt*1.5, line, "", 1, "C", false, 0, "")
}
}
// 产品详情(如果存在)
if product.Content != "" {
pdf.Ln(20)
content := pb.textProcessor.StripHTML(product.Content)
content = pb.textProcessor.CleanText(content)
pb.fontManager.SetFont(pdf, "", 12)
_, lineHt = pdf.GetFontSize()
contentWidth := pageWidth * 0.7
2025-12-03 16:53:31 +08:00
contentLines := pb.safeSplitText(pdf, content, contentWidth, chineseFontAvailable)
2025-12-03 12:03:42 +08:00
currentX := (pageWidth - contentWidth) / 2
for _, line := range contentLines {
pdf.SetX(currentX)
pdf.CellFormat(contentWidth, lineHt*1.4, line, "", 1, "C", false, 0, "")
}
}
// 底部信息(价格等)
if !product.Price.IsZero() {
pdf.SetY(pageHeight - 60)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 12)
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, fmt.Sprintf("价格:%s 元", product.Price.String()), "", 1, "C", false, 0, "")
}
}
// AddDocumentationPages 添加接口文档页面
func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
// 创建自定义的AddPage函数确保每页都有水印
addPageWithWatermark := func() {
pdf.AddPage()
pb.addHeader(pdf, chineseFontAvailable)
pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印
}
addPageWithWatermark()
pdf.SetY(45)
pb.fontManager.SetFont(pdf, "B", 18)
_, lineHt := pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "接口文档", "", 1, "L", false, 0, "")
// 请求URL
pdf.Ln(8)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pb.fontManager.SetFont(pdf, "B", 12)
pdf.CellFormat(0, lineHt, "请求URL", "", 1, "L", false, 0, "")
// URL使用黑体字体可能包含中文字符
// 先清理URL中的乱码
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
pb.fontManager.SetFont(pdf, "", 10) // 使用黑体
pdf.SetTextColor(0, 0, 0)
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
// 请求方法
pdf.Ln(5)
pb.fontManager.SetFont(pdf, "B", 12)
pdf.CellFormat(0, lineHt, fmt.Sprintf("请求方法:%s", doc.RequestMethod), "", 1, "L", false, 0, "")
// 基本信息
if doc.BasicInfo != "" {
pdf.Ln(8)
pb.addSection(pdf, "基本信息", doc.BasicInfo, chineseFontAvailable)
}
// 请求参数
if doc.RequestParams != "" {
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "B", 14)
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "请求参数:", "", 1, "L", false, 0, "")
// 使用新的数据库驱动方式处理请求参数
if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "request_params"); err != nil {
pb.logger.Warn("渲染请求参数表格失败,回退到文本显示", zap.Error(err))
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.RequestParams)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
// 生成JSON示例
if jsonExample := pb.jsonProcessor.GenerateJSONExample(doc.RequestParams, pb.tableParser); jsonExample != "" {
pdf.Ln(5)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pb.fontManager.SetFont(pdf, "B", 14)
pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "")
// JSON中可能包含中文值使用黑体字体
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON支持中文
pdf.SetTextColor(0, 0, 0)
pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false)
}
}
// 响应示例
if doc.ResponseExample != "" {
pdf.Ln(8)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pb.fontManager.SetFont(pdf, "B", 14)
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "")
// 优先尝试提取和格式化JSON
jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample)
if jsonContent != "" {
// 格式化JSON
formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent)
if err == nil {
jsonContent = formattedJSON
}
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON支持中文
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
} else {
// 如果没有JSON尝试使用表格方式处理
if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil {
pb.logger.Warn("渲染响应示例表格失败,回退到文本显示", zap.Error(err))
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseExample)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
}
// 返回字段说明
if doc.ResponseFields != "" {
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "B", 14)
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "返回字段说明:", "", 1, "L", false, 0, "")
// 使用新的数据库驱动方式处理返回字段(支持多个表格,带标题)
if err := pb.tableParser.ParseAndRenderTablesWithTitles(context.Background(), pdf, doc, "response_fields"); err != nil {
pb.logger.Warn("渲染返回字段表格失败,回退到文本显示",
zap.Error(err),
zap.String("content_preview", pb.getContentPreview(doc.ResponseFields, 200)))
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseFields)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
} else {
pb.logger.Warn("返回字段内容为空或只有空白字符")
}
} else {
pb.logger.Info("返回字段说明表格渲染成功")
}
} else {
pb.logger.Debug("返回字段内容为空,跳过渲染")
}
// 错误代码
if doc.ErrorCodes != "" {
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "B", 14)
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "错误代码:", "", 1, "L", false, 0, "")
// 使用新的数据库驱动方式处理错误代码
if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "error_codes"); err != nil {
pb.logger.Warn("渲染错误代码表格失败,回退到文本显示", zap.Error(err))
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ErrorCodes)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
}
// addSection 添加章节
func (pb *PageBuilder) addSection(pdf *gofpdf.Fpdf, title, content string, chineseFontAvailable bool) {
_, lineHt := pdf.GetFontSize()
pb.fontManager.SetFont(pdf, "B", 14)
pdf.CellFormat(0, lineHt, title+"", "", 1, "L", false, 0, "")
// 第一步预处理和转换标准化markdown格式
content = pb.markdownConverter.PreprocessContent(content)
// 第二步将内容格式化为标准的markdown表格格式如果还不是
content = pb.markdownProc.FormatContentAsMarkdownTable(content)
// 先尝试提取JSON如果是代码块格式
if jsonContent := pb.jsonProcessor.ExtractJSON(content); jsonContent != "" {
// 格式化JSON
formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent)
if err == nil {
jsonContent = formattedJSON
}
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体
pdf.MultiCell(0, lineHt*1.2, jsonContent, "", "L", false)
} else {
// 按#号标题分割内容,每个标题下的内容单独处理
sections := pb.markdownProc.SplitByMarkdownHeaders(content)
if len(sections) > 0 {
// 如果有多个章节,逐个处理
for i, section := range sections {
if i > 0 {
pdf.Ln(5) // 章节之间的间距
}
// 如果有标题,先显示标题
if section.Title != "" {
titleLevel := section.Level
fontSize := 14.0 - float64(titleLevel-2)*2 // ## 是14, ### 是12, #### 是10
if fontSize < 10 {
fontSize = 10
}
pb.fontManager.SetFont(pdf, "B", fontSize)
pdf.SetTextColor(0, 0, 0)
// 清理标题中的#号
cleanTitle := strings.TrimSpace(strings.TrimLeft(section.Title, "#"))
pdf.CellFormat(0, lineHt*1.2, cleanTitle, "", 1, "L", false, 0, "")
pdf.Ln(3)
}
// 处理该章节的内容(可能是表格或文本)
pb.processSectionContent(pdf, section.Content, chineseFontAvailable, lineHt)
}
} else {
// 如果没有标题分割,直接处理整个内容
pb.processSectionContent(pdf, content, chineseFontAvailable, lineHt)
}
}
}
// processRequestParams 处理请求参数:直接解析所有表格,确保表格能够正确渲染
func (pb *PageBuilder) processRequestParams(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) {
// 第一步预处理和转换标准化markdown格式
content = pb.markdownConverter.PreprocessContent(content)
// 第二步将数据格式化为标准的markdown表格格式
processedContent := pb.markdownProc.FormatContentAsMarkdownTable(content)
// 解析并显示所有表格(不按标题分组)
// 将内容按表格分割,找到所有表格块
allTables := pb.tableParser.ExtractAllTables(processedContent)
if len(allTables) > 0 {
// 有表格,逐个渲染
for i, tableBlock := range allTables {
if i > 0 {
pdf.Ln(5) // 表格之间的间距
}
// 渲染表格前的说明文字(包括标题)
if tableBlock.BeforeText != "" {
beforeText := tableBlock.BeforeText
// 处理标题和文本
pb.renderTextWithTitles(pdf, beforeText, chineseFontAvailable, lineHt)
pdf.Ln(3)
}
// 渲染表格
if len(tableBlock.TableData) > 0 && pb.tableParser.IsValidTable(tableBlock.TableData) {
pb.tableRenderer.RenderTable(pdf, tableBlock.TableData)
}
// 渲染表格后的说明文字
if tableBlock.AfterText != "" {
afterText := pb.textProcessor.StripHTML(tableBlock.AfterText)
afterText = pb.textProcessor.CleanText(afterText)
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
}
} else {
// 没有表格,显示为文本
text := pb.textProcessor.StripHTML(processedContent)
text = pb.textProcessor.CleanText(text)
if strings.TrimSpace(text) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
// processResponseExample 处理响应示例不按markdown标题分级直接解析所有表格但保留标题显示
func (pb *PageBuilder) processResponseExample(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) {
// 第一步预处理和转换标准化markdown格式
content = pb.markdownConverter.PreprocessContent(content)
// 第二步将数据格式化为标准的markdown表格格式
processedContent := pb.markdownProc.FormatContentAsMarkdownTable(content)
// 尝试提取JSON内容如果存在代码块
jsonContent := pb.jsonProcessor.ExtractJSON(processedContent)
if jsonContent != "" {
pdf.SetTextColor(0, 0, 0)
formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent)
if err == nil {
jsonContent = formattedJSON
}
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
pdf.Ln(5)
}
// 解析并显示所有表格(不按标题分组)
// 将内容按表格分割,找到所有表格块
allTables := pb.tableParser.ExtractAllTables(processedContent)
if len(allTables) > 0 {
// 有表格,逐个渲染
for i, tableBlock := range allTables {
if i > 0 {
pdf.Ln(5) // 表格之间的间距
}
// 渲染表格前的说明文字(包括标题)
if tableBlock.BeforeText != "" {
beforeText := tableBlock.BeforeText
// 处理标题和文本
pb.renderTextWithTitles(pdf, beforeText, chineseFontAvailable, lineHt)
pdf.Ln(3)
}
// 渲染表格
if len(tableBlock.TableData) > 0 && pb.tableParser.IsValidTable(tableBlock.TableData) {
pb.tableRenderer.RenderTable(pdf, tableBlock.TableData)
}
// 渲染表格后的说明文字
if tableBlock.AfterText != "" {
afterText := pb.textProcessor.StripHTML(tableBlock.AfterText)
afterText = pb.textProcessor.CleanText(afterText)
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
}
} else {
// 没有表格,显示为文本
text := pb.textProcessor.StripHTML(processedContent)
text = pb.textProcessor.CleanText(text)
if strings.TrimSpace(text) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
// processSectionContent 处理单个章节的内容(解析表格或显示文本)
func (pb *PageBuilder) processSectionContent(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) {
// 尝试解析markdown表格
tableData := pb.tableParser.ParseMarkdownTable(content)
// 检查内容是否包含表格标记(|符号)
hasTableMarkers := strings.Contains(content, "|")
// 如果解析出了表格数据(即使验证失败),或者内容包含表格标记,都尝试渲染表格
// 放宽条件:支持只有表头的表格(单行表格)
if len(tableData) >= 1 && hasTableMarkers {
// 如果表格有效,或者至少有表头,都尝试渲染
if pb.tableParser.IsValidTable(tableData) {
// 如果是有效的表格,先检查表格前后是否有说明文字
// 提取表格前后的文本(用于显示说明)
lines := strings.Split(content, "\n")
var beforeTable []string
var afterTable []string
inTable := false
tableStartLine := -1
tableEndLine := -1
// 找到表格的起始和结束行
usePipeDelimiter := false
for _, line := range lines {
if strings.Contains(strings.TrimSpace(line), "|") {
usePipeDelimiter = true
break
}
}
for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
if usePipeDelimiter && strings.Contains(trimmedLine, "|") {
if !inTable {
tableStartLine = i
inTable = true
}
tableEndLine = i
} else if inTable && usePipeDelimiter && !strings.Contains(trimmedLine, "|") {
// 表格可能结束了
if strings.HasPrefix(trimmedLine, "```") {
tableEndLine = i - 1
break
}
}
}
// 提取表格前的文本
if tableStartLine > 0 {
beforeTable = lines[0:tableStartLine]
}
// 提取表格后的文本
if tableEndLine >= 0 && tableEndLine < len(lines)-1 {
afterTable = lines[tableEndLine+1:]
}
// 显示表格前的说明文字
if len(beforeTable) > 0 {
beforeText := strings.Join(beforeTable, "\n")
beforeText = pb.textProcessor.StripHTML(beforeText)
beforeText = pb.textProcessor.CleanText(beforeText)
if strings.TrimSpace(beforeText) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, beforeText, "", "L", false)
pdf.Ln(3)
}
}
// 渲染表格
pb.tableRenderer.RenderTable(pdf, tableData)
// 显示表格后的说明文字
if len(afterTable) > 0 {
afterText := strings.Join(afterTable, "\n")
afterText = pb.textProcessor.StripHTML(afterText)
afterText = pb.textProcessor.CleanText(afterText)
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
}
} else {
// 如果不是有效表格显示为文本完整显示markdown内容
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
text := pb.textProcessor.StripHTML(content)
text = pb.textProcessor.CleanText(text) // 清理无效字符,保留中文
// 如果文本不为空,显示它
if strings.TrimSpace(text) != "" {
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
// renderTextWithTitles 渲染包含markdown标题的文本
func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chineseFontAvailable bool, lineHt float64) {
lines := strings.Split(text, "\n")
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
// 检查是否是标题行
if strings.HasPrefix(trimmedLine, "#") {
// 计算标题级别
level := 0
for _, r := range trimmedLine {
if r == '#' {
level++
} else {
break
}
}
// 提取标题文本(移除#号)
titleText := strings.TrimSpace(trimmedLine[level:])
if titleText == "" {
continue
}
// 根据级别设置字体大小
fontSize := 14.0 - float64(level-2)*2
if fontSize < 10 {
fontSize = 10
}
if fontSize > 16 {
fontSize = 16
}
// 渲染标题
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "B", fontSize)
_, titleLineHt := pdf.GetFontSize()
pdf.CellFormat(0, titleLineHt*1.2, titleText, "", 1, "L", false, 0, "")
pdf.Ln(2)
} else if strings.TrimSpace(line) != "" {
// 普通文本行只去除HTML标签保留markdown格式
cleanText := pb.textProcessor.StripHTML(line)
cleanText = pb.textProcessor.CleanTextPreservingMarkdown(cleanText)
if strings.TrimSpace(cleanText) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pdf.MultiCell(0, lineHt*1.3, cleanText, "", "L", false)
}
} else {
// 空行,添加间距
pdf.Ln(2)
}
}
}
// addHeader 添加页眉logo和文字
func (pb *PageBuilder) addHeader(pdf *gofpdf.Fpdf, chineseFontAvailable bool) {
pdf.SetY(5)
// 绘制logo如果存在
if pb.logoPath != "" {
if _, err := os.Stat(pb.logoPath); err == nil {
// gofpdf的ImageOptions方法调整位置和大小左边距是15mm
pdf.ImageOptions(pb.logoPath, 15, 5, 15, 15, false, gofpdf.ImageOptions{}, 0, "")
pb.logger.Info("已添加logo", zap.String("path", pb.logoPath))
} else {
pb.logger.Warn("logo文件不存在", zap.String("path", pb.logoPath), zap.Error(err))
}
} else {
pb.logger.Warn("logo路径为空")
}
// 绘制"天远数据"文字(使用中文字体如果可用)
pdf.SetXY(33, 8)
pb.fontManager.SetFont(pdf, "B", 14)
pdf.CellFormat(0, 10, "天远数据", "", 0, "L", false, 0, "")
// 绘制下横线优化位置左边距是15mm
pdf.Line(15, 22, 75, 22)
}
// addWatermark 添加水印从左边开始向上倾斜45度考虑可用区域
func (pb *PageBuilder) addWatermark(pdf *gofpdf.Fpdf, chineseFontAvailable bool) {
// 如果中文字体不可用,跳过水印(避免显示乱码)
if !chineseFontAvailable {
return
}
// 保存当前图形状态
pdf.TransformBegin()
defer pdf.TransformEnd()
// 获取页面尺寸和边距
_, pageHeight := pdf.GetPageSize()
leftMargin, topMargin, _, bottomMargin := pdf.GetMargins()
// 计算实际可用区域高度
usableHeight := pageHeight - topMargin - bottomMargin
// 设置水印样式(使用水印字体,非黑体)
fontSize := 45.0
pb.fontManager.SetWatermarkFont(pdf, "", fontSize)
// 设置灰色和透明度(加深水印,使其更明显)
pdf.SetTextColor(180, 180, 180) // 深一点的灰色
pdf.SetAlpha(0.25, "Normal") // 增加透明度,让水印更明显
// 计算文字宽度
textWidth := pdf.GetStringWidth(pb.watermarkText)
if textWidth == 0 {
// 如果无法获取宽度(字体未注册),使用估算值(中文字符大约每个 fontSize/3 mm
textWidth = float64(len([]rune(pb.watermarkText))) * fontSize / 3.0
}
// 从左边开始,计算起始位置
// 起始X左边距
// 起始Y考虑水印文字长度和旋转后需要的空间
startX := leftMargin
startY := topMargin + textWidth*0.5 // 为旋转留出空间
// 移动到起始位置
pdf.TransformTranslate(startX, startY)
// 向上倾斜45度顺时针旋转45度即-45度或逆时针315度
pdf.TransformRotate(-45, 0, 0)
// 检查文字是否会超出可用区域(旋转后的对角线长度)
rotatedDiagonal := math.Sqrt(textWidth*textWidth + fontSize*fontSize)
if rotatedDiagonal > usableHeight*0.8 {
// 如果太大,缩小字体
fontSize = fontSize * usableHeight * 0.8 / rotatedDiagonal
pb.fontManager.SetWatermarkFont(pdf, "", fontSize)
textWidth = pdf.GetStringWidth(pb.watermarkText)
if textWidth == 0 {
textWidth = float64(len([]rune(pb.watermarkText))) * fontSize / 3.0
}
}
// 从左边开始绘制水印文字
pdf.SetXY(0, 0)
pdf.CellFormat(textWidth, fontSize, pb.watermarkText, "", 0, "L", false, 0, "")
// 恢复透明度和颜色
pdf.SetAlpha(1.0, "Normal")
pdf.SetTextColor(0, 0, 0) // 恢复为黑色
}
// getContentPreview 获取内容预览(用于日志记录)
func (pb *PageBuilder) getContentPreview(content string, maxLen int) string {
content = strings.TrimSpace(content)
if len(content) <= maxLen {
return content
}
return content[:maxLen] + "..."
}
2025-12-03 16:53:31 +08:00
// safeSplitText 安全地分割文本避免在没有中文字体时调用SplitText导致panic
func (pb *PageBuilder) safeSplitText(pdf *gofpdf.Fpdf, text string, width float64, chineseFontAvailable bool) []string {
// 检查文本是否包含中文字符
hasChinese := false
for _, r := range text {
if (r >= 0x4E00 && r <= 0x9FFF) || // 中文字符范围
(r >= 0x3400 && r <= 0x4DBF) || // 扩展A
(r >= 0x20000 && r <= 0x2A6DF) || // 扩展B
(r >= 0x3000 && r <= 0x303F) || // CJK符号和标点
(r >= 0xFF00 && r <= 0xFFEF) { // 全角字符
hasChinese = true
break
}
}
// 如果文本包含中文但中文字体不可用,直接使用手动分割方法
// 如果中文字体可用且文本不包含中文可以尝试使用SplitText
// 即使中文字体可用,如果文本包含中文,也要小心处理(字体可能未正确加载)
if chineseFontAvailable && !hasChinese {
// 对于纯英文/ASCII文本可以安全使用SplitText
var lines []string
func() {
defer func() {
if r := recover(); r != nil {
pb.logger.Warn("SplitText发生panic使用备用方法", zap.Any("error", r))
}
}()
lines = pdf.SplitText(text, width)
}()
if len(lines) > 0 {
return lines
}
}
// 使用手动分割方法适用于中文文本或SplitText失败的情况
// 估算字符宽度中文字符约6mm英文字符约3mm基于14号字体
fontSize, _ := pdf.GetFontSize()
chineseCharWidth := fontSize * 0.43 // 中文字符宽度mm
englishCharWidth := fontSize * 0.22 // 英文字符宽度mm
var lines []string
var currentLine strings.Builder
currentWidth := 0.0
runes := []rune(text)
for _, r := range runes {
// 计算字符宽度
var charWidth float64
if (r >= 0x4E00 && r <= 0x9FFF) || // 中文字符范围
(r >= 0x3400 && r <= 0x4DBF) || // 扩展A
(r >= 0x20000 && r <= 0x2A6DF) || // 扩展B
(r >= 0x3000 && r <= 0x303F) || // CJK符号和标点
(r >= 0xFF00 && r <= 0xFFEF) { // 全角字符
charWidth = chineseCharWidth
} else {
charWidth = englishCharWidth
}
// 检查是否需要换行
if currentWidth+charWidth > width && currentLine.Len() > 0 {
// 保存当前行
lines = append(lines, currentLine.String())
currentLine.Reset()
currentWidth = 0.0
}
// 处理换行符
if r == '\n' {
if currentLine.Len() > 0 {
lines = append(lines, currentLine.String())
currentLine.Reset()
currentWidth = 0.0
} else {
// 空行
lines = append(lines, "")
}
continue
}
// 添加字符到当前行
currentLine.WriteRune(r)
currentWidth += charWidth
}
// 添加最后一行
if currentLine.Len() > 0 {
lines = append(lines, currentLine.String())
}
// 如果没有分割出任何行,至少返回一行
if len(lines) == 0 {
lines = []string{text}
}
return lines
}