Files
tyapi-server/internal/shared/pdf/table_renderer.go
2025-12-03 12:03:42 +08:00

341 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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