341 lines
10 KiB
Go
341 lines
10 KiB
Go
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)
|
||
}
|
||
}
|