827 lines
27 KiB
Go
827 lines
27 KiB
Go
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.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 {
|
||
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
|
||
}
|