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 }