This commit is contained in:
2025-12-03 12:03:42 +08:00
parent 1cf64e831c
commit 63252fa30f
27 changed files with 7167 additions and 36 deletions

View File

@@ -0,0 +1,340 @@
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)
}
}