package pdf import ( "math" "strings" "github.com/jung-kurt/gofpdf/v2" "go.uber.org/zap" ) // min 返回两个整数中的较小值 func min(a, b int) int { if a < b { return a } return b } // TableRenderer 表格渲染器 type TableRenderer struct { logger *zap.Logger fontManager *FontManager textProcessor *TextProcessor } // NewTableRenderer 创建表格渲染器 func NewTableRenderer(logger *zap.Logger, fontManager *FontManager, textProcessor *TextProcessor) *TableRenderer { return &TableRenderer{ logger: logger, fontManager: fontManager, textProcessor: textProcessor, } } // RenderTable 渲染表格 func (tr *TableRenderer) RenderTable(pdf *gofpdf.Fpdf, tableData [][]string) { if len(tableData) == 0 { return } // 支持只有表头的表格(单行表格) if len(tableData) == 1 { tr.logger.Info("渲染单行表格(只有表头)", zap.Int("cols", len(tableData[0]))) } _, lineHt := pdf.GetFontSize() tr.fontManager.SetFont(pdf, "", 9) // 计算列宽(根据内容动态计算,确保所有列都能显示) pageWidth, _ := pdf.GetPageSize() availableWidth := pageWidth - 30 // 减去左右边距(15mm * 2) numCols := len(tableData[0]) // 计算每列的最小宽度(根据内容) colMinWidths := make([]float64, numCols) tr.fontManager.SetFont(pdf, "", 9) // 遍历所有行,计算每列的最大内容宽度 for i, row := range tableData { for j := 0; j < numCols && j < len(row); j++ { cell := tr.textProcessor.CleanTextPreservingMarkdown(row[j]) // 计算文本宽度 var textWidth float64 if tr.fontManager.IsChineseFontAvailable() { textWidth = pdf.GetStringWidth(cell) } else { // 估算宽度 charCount := len([]rune(cell)) textWidth = float64(charCount) * 3.0 // 估算每个字符3mm } // 加上边距(左右各4mm,进一步增加边距让内容更舒适) cellWidth := textWidth + 8 // 最小宽度(表头可能需要更多空间) if i == 0 { cellWidth = math.Max(cellWidth, 30) // 表头最小30mm(从25mm增加) } else { cellWidth = math.Max(cellWidth, 25) // 数据行最小25mm(从20mm增加) } if cellWidth > colMinWidths[j] { colMinWidths[j] = cellWidth } } } // 确保所有列的最小宽度一致(避免宽度差异过大) minColWidth := 25.0 for i := range colMinWidths { if colMinWidths[i] < minColWidth { colMinWidths[i] = minColWidth } } // 计算总的最小宽度 totalMinWidth := 0.0 for _, w := range colMinWidths { totalMinWidth += w } // 计算每列的实际宽度 colWidths := make([]float64, numCols) if totalMinWidth <= availableWidth { // 如果总宽度不超过可用宽度,使用计算的最小宽度,剩余空间平均分配 extraWidth := availableWidth - totalMinWidth extraPerCol := extraWidth / float64(numCols) for i := range colWidths { colWidths[i] = colMinWidths[i] + extraPerCol } } else { // 如果总宽度超过可用宽度,按比例缩放 scale := availableWidth / totalMinWidth for i := range colWidths { colWidths[i] = colMinWidths[i] * scale // 确保最小宽度 if colWidths[i] < 10 { colWidths[i] = 10 } } // 重新调整以确保总宽度不超过可用宽度 actualTotal := 0.0 for _, w := range colWidths { actualTotal += w } if actualTotal > availableWidth { scale = availableWidth / actualTotal for i := range colWidths { colWidths[i] *= scale } } } // 绘制表头 header := tableData[0] pdf.SetFillColor(74, 144, 226) // 蓝色背景 pdf.SetTextColor(0, 0, 0) // 黑色文字 tr.fontManager.SetFont(pdf, "B", 9) // 清理表头文本(只清理无效字符,保留markdown格式) for i, cell := range header { header[i] = tr.textProcessor.CleanTextPreservingMarkdown(cell) } // 先计算表头的最大高度 headerStartY := pdf.GetY() maxHeaderHeight := lineHt * 2.5 // 进一步增加表头高度,从2.0倍增加到2.5倍 for i, cell := range header { if i >= len(colWidths) { break } colW := colWidths[i] headerLines := pdf.SplitText(cell, colW-6) // 增加边距,从4增加到6 headerHeight := float64(len(headerLines)) * lineHt * 2.5 // 进一步增加表头行高 if headerHeight < lineHt*2.5 { headerHeight = lineHt * 2.5 } if headerHeight > maxHeaderHeight { maxHeaderHeight = headerHeight } } // 绘制表头(使用动态计算的列宽) currentX := 15.0 for i, cell := range header { if i >= len(colWidths) { break } colW := colWidths[i] // 绘制表头背景 pdf.Rect(currentX, headerStartY, colW, maxHeaderHeight, "FD") // 绘制表头文本(不使用ClipRect,直接使用MultiCell,它会自动处理换行) // 确保文本不为空 if strings.TrimSpace(cell) != "" { // 增加内边距,从2增加到3 pdf.SetXY(currentX+3, headerStartY+3) // 确保表头文字为黑色 pdf.SetTextColor(0, 0, 0) // 进一步增加表头行高,从2.0倍增加到2.5倍 pdf.MultiCell(colW-6, lineHt*2.5, cell, "", "C", false) } else { // 如果单元格为空,记录警告 tr.logger.Warn("表头单元格为空", zap.Int("col_index", i), zap.String("header", strings.Join(header, ","))) } // 重置Y坐标,确保下一列从同一行开始 pdf.SetXY(currentX+colW, headerStartY) currentX += colW } // 移动到下一行(使用计算好的最大表头高度) pdf.SetXY(15.0, headerStartY+maxHeaderHeight) // 绘制数据行 pdf.SetFillColor(245, 245, 220) // 米色背景 pdf.SetTextColor(0, 0, 0) // 深黑色文字,确保清晰 tr.fontManager.SetFont(pdf, "", 9) _, lineHt = pdf.GetFontSize() for i := 1; i < len(tableData); i++ { row := tableData[i] fill := (i % 2) == 0 // 交替填充 // 计算这一行的起始Y坐标 startY := pdf.GetY() // 设置字体以计算文本宽度和高度 tr.fontManager.SetFont(pdf, "", 9) _, cellLineHt := pdf.GetFontSize() // 先遍历一次,计算每列需要的最大高度 maxCellHeight := cellLineHt * 2.5 // 进一步增加最小高度,从2.0倍增加到2.5倍 for j, cell := range row { if j >= numCols || j >= len(colWidths) { break } // 清理单元格文本(只清理无效字符,保留markdown格式) cleanCell := tr.textProcessor.CleanTextPreservingMarkdown(cell) cellWidth := colWidths[j] - 6 // 使用动态计算的列宽,减去左右边距(从4增加到6) // 使用SplitText准确计算需要的行数 var lines []string if tr.fontManager.IsChineseFontAvailable() { // 对于中文字体,使用SplitText lines = pdf.SplitText(cleanCell, cellWidth) } else { // 对于Arial字体,如果包含中文可能失败,使用估算 charCount := len([]rune(cleanCell)) if charCount == 0 { lines = []string{""} } else { // 中文字符宽度大约是英文字符的2倍 estimatedWidth := 0.0 for _, r := range cleanCell { if r >= 0x4E00 && r <= 0x9FFF { estimatedWidth += 6.0 // 中文字符宽度 } else { estimatedWidth += 3.0 // 英文字符宽度 } } estimatedLines := math.Ceil(estimatedWidth / cellWidth) if estimatedLines < 1 { estimatedLines = 1 } lines = make([]string, int(estimatedLines)) // 简单分割文本 charsPerLine := int(math.Ceil(float64(charCount) / estimatedLines)) for k := 0; k < int(estimatedLines); k++ { start := k * charsPerLine end := start + charsPerLine if end > charCount { end = charCount } if start < charCount { runes := []rune(cleanCell) if start < len(runes) { if end > len(runes) { end = len(runes) } lines[k] = string(runes[start:end]) } } } } } // 计算单元格高度 numLines := float64(len(lines)) if numLines == 0 { numLines = 1 } cellHeight := numLines * cellLineHt * 2.5 // 进一步增加行高,从2.0倍增加到2.5倍 if cellHeight < cellLineHt*2.5 { cellHeight = cellLineHt * 2.5 } // 为多行内容添加额外间距 if len(lines) > 1 { cellHeight += cellLineHt * 0.5 // 多行时额外增加0.5倍行高 } if cellHeight > maxCellHeight { maxCellHeight = cellHeight } } // 绘制这一行的所有单元格(左边距是15mm) currentX := 15.0 for j, cell := range row { if j >= numCols || j >= len(colWidths) { break } colW := colWidths[j] // 使用动态计算的列宽 // 绘制单元格边框和背景 if fill { pdf.SetFillColor(250, 250, 235) // 稍深的米色 } else { pdf.SetFillColor(255, 255, 255) } pdf.Rect(currentX, startY, colW, maxCellHeight, "FD") // 绘制文本(使用MultiCell支持换行,并限制在单元格内) pdf.SetTextColor(0, 0, 0) // 确保深黑色 // 只清理无效字符,保留markdown格式 cleanCell := tr.textProcessor.CleanTextPreservingMarkdown(cell) // 确保文本不为空才渲染 if strings.TrimSpace(cleanCell) != "" { // 设置到单元格内,增加边距(从2增加到3),让内容更舒适 pdf.SetXY(currentX+3, startY+3) // 使用MultiCell自动换行,左对齐 tr.fontManager.SetFont(pdf, "", 9) // 再次确保颜色为深黑色(防止被其他设置覆盖) pdf.SetTextColor(0, 0, 0) // 设置字体后再次确保颜色 pdf.SetTextColor(0, 0, 0) // 使用MultiCell,会自动处理换行(使用统一的行高) // MultiCell会自动处理换行,不需要ClipRect // 进一步增加行高,从2.0倍增加到2.5倍,让内容更舒适 pdf.MultiCell(colW-6, cellLineHt*2.5, cleanCell, "", "L", false) } else if strings.TrimSpace(cell) != "" { // 如果原始单元格不为空但清理后为空,记录警告 tr.logger.Warn("单元格文本清理后为空", zap.Int("row", i), zap.Int("col", j), zap.String("original", cell[:min(len(cell), 50)])) } // MultiCell后Y坐标已经改变,必须重置以便下一列从同一行开始 // 这是关键:确保所有列都从同一个startY开始 pdf.SetXY(currentX+colW, startY) // 移动到下一列 currentX += colW } // 移动到下一行的起始位置(使用计算好的最大高度) pdf.SetXY(15.0, startY+maxCellHeight) } }