Files
tyapi-server/internal/shared/pdf/page_builder.go
2025-12-04 10:47:58 +08:00

825 lines
27 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 (
"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
descLines := pb.safeSplitText(pdf, desc, descWidth, chineseFontAvailable)
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
contentLines := pb.safeSplitText(pdf, content, contentWidth, chineseFontAvailable)
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.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 {
pdf.ImageOptions(pb.logoPath, 15, 5, 15, 15, false, gofpdf.ImageOptions{}, 0, "")
} else {
pb.logger.Warn("logo文件不存在", zap.String("path", pb.logoPath))
}
}
// 绘制"天远数据"文字(使用中文字体如果可用)
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] + "..."
}
// 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
}