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)
|
|||
|
|
}
|
|||
|
|
}
|