644 lines
18 KiB
Go
644 lines
18 KiB
Go
package pdf
|
||
|
||
import (
|
||
"math"
|
||
"regexp"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"github.com/jung-kurt/gofpdf/v2"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// DatabaseTableRenderer 数据库表格渲染器
|
||
type DatabaseTableRenderer struct {
|
||
logger *zap.Logger
|
||
fontManager *FontManager
|
||
}
|
||
|
||
// NewDatabaseTableRenderer 创建数据库表格渲染器
|
||
func NewDatabaseTableRenderer(logger *zap.Logger, fontManager *FontManager) *DatabaseTableRenderer {
|
||
return &DatabaseTableRenderer{
|
||
logger: logger,
|
||
fontManager: fontManager,
|
||
}
|
||
}
|
||
|
||
// RenderTable 渲染表格到PDF
|
||
func (r *DatabaseTableRenderer) RenderTable(pdf *gofpdf.Fpdf, tableData *TableData) error {
|
||
if tableData == nil || len(tableData.Headers) == 0 {
|
||
r.logger.Warn("表格数据为空,跳过渲染")
|
||
return nil
|
||
}
|
||
|
||
// 检查表头是否有有效内容
|
||
hasValidHeader := false
|
||
for _, header := range tableData.Headers {
|
||
if strings.TrimSpace(header) != "" {
|
||
hasValidHeader = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if !hasValidHeader {
|
||
r.logger.Warn("表头内容为空,跳过渲染")
|
||
return nil
|
||
}
|
||
|
||
// 检查是否有有效的数据行
|
||
hasValidRows := false
|
||
for _, row := range tableData.Rows {
|
||
for _, cell := range row {
|
||
if strings.TrimSpace(cell) != "" {
|
||
hasValidRows = true
|
||
break
|
||
}
|
||
}
|
||
if hasValidRows {
|
||
break
|
||
}
|
||
}
|
||
|
||
r.logger.Debug("表格验证通过",
|
||
zap.Bool("has_valid_header", hasValidHeader),
|
||
zap.Bool("has_valid_rows", hasValidRows),
|
||
zap.Int("row_count", len(tableData.Rows)))
|
||
|
||
// 即使没有数据行,也渲染表头(单行表格)
|
||
// 但如果没有表头也没有数据,则不渲染
|
||
|
||
// 设置字体
|
||
r.fontManager.SetFont(pdf, "", 9)
|
||
_, lineHt := pdf.GetFontSize()
|
||
|
||
// 计算页面可用宽度
|
||
pageWidth, _ := pdf.GetPageSize()
|
||
availableWidth := pageWidth - 30 // 减去左右边距(15mm * 2)
|
||
|
||
// 计算每列宽度
|
||
colWidths := r.calculateColumnWidths(pdf, tableData, availableWidth)
|
||
|
||
// 检查是否需要分页(在绘制表头前)
|
||
_, pageHeight := pdf.GetPageSize()
|
||
_, _, _, bottomMargin := pdf.GetMargins()
|
||
currentY := pdf.GetY()
|
||
estimatedHeaderHeight := lineHt * 2.5 // 估算表头高度
|
||
|
||
if currentY+estimatedHeaderHeight > pageHeight-bottomMargin {
|
||
r.logger.Debug("表头前需要分页", zap.Float64("current_y", currentY))
|
||
pdf.AddPage()
|
||
}
|
||
|
||
// 绘制表头
|
||
headerStartY := pdf.GetY()
|
||
headerHeight := r.renderHeader(pdf, tableData.Headers, colWidths, headerStartY)
|
||
|
||
// 移动到表头下方
|
||
pdf.SetXY(15.0, headerStartY+headerHeight)
|
||
|
||
// 绘制数据行(如果有数据行)
|
||
if len(tableData.Rows) > 0 {
|
||
r.logger.Debug("开始渲染数据行", zap.Int("row_count", len(tableData.Rows)))
|
||
r.renderRows(pdf, tableData.Rows, colWidths, lineHt, tableData.Headers, colWidths)
|
||
} else {
|
||
r.logger.Debug("没有数据行,只渲染表头")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// calculateColumnWidths 计算每列的宽度
|
||
// 策略:先确保每列最短内容能完整显示(不换行),然后根据内容长度分配剩余空间
|
||
func (r *DatabaseTableRenderer) calculateColumnWidths(pdf *gofpdf.Fpdf, tableData *TableData, availableWidth float64) []float64 {
|
||
numCols := len(tableData.Headers)
|
||
r.fontManager.SetFont(pdf, "", 9)
|
||
|
||
// 第一步:找到每列中最短的内容(包括表头),计算其完整显示所需的最小宽度
|
||
colMinWidths := make([]float64, numCols)
|
||
colMaxWidths := make([]float64, numCols)
|
||
colContentLengths := make([]float64, numCols) // 用于记录内容总长度,用于分配剩余空间
|
||
|
||
for i := 0; i < numCols; i++ {
|
||
minWidth := math.MaxFloat64
|
||
maxWidth := 0.0
|
||
totalLength := 0.0
|
||
count := 0
|
||
|
||
// 检查表头
|
||
if i < len(tableData.Headers) {
|
||
header := tableData.Headers[i]
|
||
textWidth := r.getTextWidth(pdf, header)
|
||
cellWidth := textWidth + 8 // 加上内边距
|
||
if cellWidth < minWidth {
|
||
minWidth = cellWidth
|
||
}
|
||
if cellWidth > maxWidth {
|
||
maxWidth = cellWidth
|
||
}
|
||
totalLength += textWidth
|
||
count++
|
||
}
|
||
|
||
// 检查所有数据行
|
||
for _, row := range tableData.Rows {
|
||
if i < len(row) {
|
||
cell := row[i]
|
||
textWidth := r.getTextWidth(pdf, cell)
|
||
cellWidth := textWidth + 8 // 加上内边距
|
||
if cellWidth < minWidth {
|
||
minWidth = cellWidth
|
||
}
|
||
if cellWidth > maxWidth {
|
||
maxWidth = cellWidth
|
||
}
|
||
totalLength += textWidth
|
||
count++
|
||
}
|
||
}
|
||
|
||
// 设置最小宽度(确保最短内容能完整显示)
|
||
if minWidth == math.MaxFloat64 {
|
||
colMinWidths[i] = 30.0 // 默认最小宽度
|
||
} else {
|
||
colMinWidths[i] = math.Max(minWidth, 25.0) // 至少25mm
|
||
}
|
||
|
||
colMaxWidths[i] = maxWidth
|
||
if count > 0 {
|
||
colContentLengths[i] = totalLength / float64(count) // 平均内容长度
|
||
} else {
|
||
colContentLengths[i] = colMinWidths[i]
|
||
}
|
||
}
|
||
|
||
// 第二步:计算总的最小宽度(确保所有最短内容都能显示)
|
||
totalMinWidth := 0.0
|
||
for _, w := range colMinWidths {
|
||
totalMinWidth += w
|
||
}
|
||
|
||
// 第三步:分配宽度
|
||
colWidths := make([]float64, numCols)
|
||
if totalMinWidth >= availableWidth {
|
||
// 如果最小宽度已经超过可用宽度,按比例缩放,但确保每列至少能显示最短内容
|
||
scale := availableWidth / totalMinWidth
|
||
for i := range colWidths {
|
||
colWidths[i] = colMinWidths[i] * scale
|
||
// 确保最小宽度,但允许稍微压缩
|
||
if colWidths[i] < colMinWidths[i]*0.8 {
|
||
colWidths[i] = colMinWidths[i] * 0.8
|
||
}
|
||
}
|
||
} else {
|
||
// 如果最小宽度小于可用宽度,先分配最小宽度,然后根据内容长度分配剩余空间
|
||
extraWidth := availableWidth - totalMinWidth
|
||
|
||
// 计算总的内容长度(用于按比例分配)
|
||
totalContentLength := 0.0
|
||
for _, length := range colContentLengths {
|
||
totalContentLength += length
|
||
}
|
||
|
||
// 如果总内容长度为0,平均分配
|
||
if totalContentLength < 0.1 {
|
||
extraPerCol := extraWidth / float64(numCols)
|
||
for i := range colWidths {
|
||
colWidths[i] = colMinWidths[i] + extraPerCol
|
||
}
|
||
} else {
|
||
// 根据内容长度按比例分配剩余空间
|
||
for i := range colWidths {
|
||
// 计算这一列应该分配多少额外空间(基于内容长度)
|
||
ratio := colContentLengths[i] / totalContentLength
|
||
extraForCol := extraWidth * ratio
|
||
colWidths[i] = colMinWidths[i] + extraForCol
|
||
}
|
||
}
|
||
}
|
||
|
||
return colWidths
|
||
}
|
||
|
||
// cleanTextForPDF 清理文本,移除可能导致PDF生成问题的字符
|
||
func (r *DatabaseTableRenderer) cleanTextForPDF(text string) string {
|
||
// 先移除HTML标签
|
||
text = strings.TrimSpace(text)
|
||
text = strings.ReplaceAll(text, "<br>", " ")
|
||
text = strings.ReplaceAll(text, "<br/>", " ")
|
||
text = strings.ReplaceAll(text, "<br />", " ")
|
||
|
||
// 移除控制字符(除了换行符和制表符)
|
||
var cleaned strings.Builder
|
||
for _, r := range text {
|
||
// 保留可打印字符、空格、换行符、制表符
|
||
if unicode.IsPrint(r) || r == '\n' || r == '\t' || r == '\r' {
|
||
cleaned.WriteRune(r)
|
||
} else if unicode.IsSpace(r) {
|
||
// 将其他空白字符转换为空格
|
||
cleaned.WriteRune(' ')
|
||
}
|
||
}
|
||
text = cleaned.String()
|
||
|
||
// 移除多余的空白字符
|
||
text = regexp.MustCompile(`\s+`).ReplaceAllString(text, " ")
|
||
|
||
// 确保文本不为空且长度合理
|
||
if len([]rune(text)) > 10000 {
|
||
// 如果文本过长,截断
|
||
runes := []rune(text)
|
||
text = string(runes[:10000]) + "..."
|
||
}
|
||
|
||
return strings.TrimSpace(text)
|
||
}
|
||
|
||
// safeSplitText 安全地调用SplitText,带错误恢复
|
||
func (r *DatabaseTableRenderer) safeSplitText(pdf *gofpdf.Fpdf, text string, width float64) []string {
|
||
// 先清理文本
|
||
text = r.cleanTextForPDF(text)
|
||
|
||
// 如果文本为空或宽度无效,返回单行
|
||
if text == "" || width <= 0 {
|
||
if text == "" {
|
||
return []string{""}
|
||
}
|
||
return []string{text}
|
||
}
|
||
|
||
// 检查文本长度,如果太短可能不需要分割
|
||
if len([]rune(text)) <= 1 {
|
||
return []string{text}
|
||
}
|
||
|
||
// 使用匿名函数和recover保护SplitText调用
|
||
var lines []string
|
||
var panicOccurred bool
|
||
func() {
|
||
defer func() {
|
||
if rec := recover(); rec != nil {
|
||
panicOccurred = true
|
||
// 静默处理,不记录日志
|
||
// 如果panic发生,使用估算值
|
||
charCount := len([]rune(text))
|
||
if charCount == 0 {
|
||
lines = []string{""}
|
||
return
|
||
}
|
||
estimatedLines := math.Max(1, math.Ceil(float64(charCount)/20))
|
||
lines = make([]string, int(estimatedLines))
|
||
if estimatedLines == 1 {
|
||
lines[0] = text
|
||
} else {
|
||
// 简单分割文本
|
||
runes := []rune(text)
|
||
charsPerLine := int(math.Ceil(float64(len(runes)) / estimatedLines))
|
||
for i := 0; i < int(estimatedLines); i++ {
|
||
start := i * charsPerLine
|
||
end := start + charsPerLine
|
||
if end > len(runes) {
|
||
end = len(runes)
|
||
}
|
||
if start < len(runes) {
|
||
lines[i] = string(runes[start:end])
|
||
} else {
|
||
lines[i] = ""
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
// 尝试调用SplitText
|
||
lines = pdf.SplitText(text, width)
|
||
}()
|
||
|
||
// 如果panic发生或lines为nil或空,使用后备方案
|
||
if panicOccurred || lines == nil || len(lines) == 0 {
|
||
// 如果文本不为空,至少返回一行
|
||
if text != "" {
|
||
return []string{text}
|
||
}
|
||
return []string{""}
|
||
}
|
||
|
||
// 过滤掉空行(但保留至少一行)
|
||
nonEmptyLines := make([]string, 0, len(lines))
|
||
for _, line := range lines {
|
||
if strings.TrimSpace(line) != "" {
|
||
nonEmptyLines = append(nonEmptyLines, line)
|
||
}
|
||
}
|
||
if len(nonEmptyLines) == 0 {
|
||
return []string{text}
|
||
}
|
||
|
||
return nonEmptyLines
|
||
}
|
||
|
||
// renderHeader 渲染表头
|
||
func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string, colWidths []float64, startY float64) float64 {
|
||
_, lineHt := pdf.GetFontSize()
|
||
|
||
// 计算表头的最大高度
|
||
maxHeaderHeight := lineHt * 2.0 // 使用合理的表头高度
|
||
for i, header := range headers {
|
||
if i >= len(colWidths) {
|
||
break
|
||
}
|
||
colW := colWidths[i]
|
||
// 使用安全的SplitText方法
|
||
headerLines := r.safeSplitText(pdf, header, colW-6)
|
||
headerHeight := float64(len(headerLines)) * lineHt
|
||
// 添加上下内边距
|
||
headerHeight += lineHt * 0.8 // 上下各0.4倍行高的内边距
|
||
if headerHeight < lineHt*2.0 {
|
||
headerHeight = lineHt * 2.0
|
||
}
|
||
if headerHeight > maxHeaderHeight {
|
||
maxHeaderHeight = headerHeight
|
||
}
|
||
}
|
||
|
||
// 绘制表头背景和文本
|
||
pdf.SetFillColor(74, 144, 226) // 蓝色背景
|
||
pdf.SetTextColor(0, 0, 0) // 黑色文字
|
||
r.fontManager.SetFont(pdf, "B", 9)
|
||
|
||
currentX := 15.0
|
||
for i, header := range headers {
|
||
if i >= len(colWidths) {
|
||
break
|
||
}
|
||
colW := colWidths[i]
|
||
|
||
// 清理表头数据,移除任何残留的HTML标签和样式
|
||
header = r.cleanTextForPDF(header)
|
||
|
||
// 绘制表头背景
|
||
pdf.Rect(currentX, startY, colW, maxHeaderHeight, "FD")
|
||
|
||
// 绘制表头文本
|
||
if header != "" {
|
||
// 计算文本的实际高度(减少内边距,给文本更多空间)
|
||
// 使用安全的SplitText方法
|
||
headerLines := r.safeSplitText(pdf, header, colW-4)
|
||
textHeight := float64(len(headerLines)) * lineHt
|
||
if textHeight < lineHt {
|
||
textHeight = lineHt
|
||
}
|
||
|
||
// 计算垂直居中的Y位置
|
||
cellCenterY := startY + maxHeaderHeight/2
|
||
textStartY := cellCenterY - textHeight/2
|
||
|
||
// 设置文本位置(水平居中,垂直居中,减少左边距)
|
||
pdf.SetXY(currentX+2, textStartY)
|
||
// 确保颜色为深黑色(在渲染前再次设置,防止被覆盖)
|
||
pdf.SetTextColor(0, 0, 0) // 表头是黑色文字
|
||
// 设置字体,确保颜色不会变淡
|
||
r.fontManager.SetFont(pdf, "B", 9)
|
||
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
||
pdf.SetTextColor(0, 0, 0)
|
||
// 使用正常的行高,文本已经垂直居中(减少内边距,给文本更多空间)
|
||
pdf.MultiCell(colW-4, lineHt, header, "", "C", false)
|
||
}
|
||
|
||
// 重置Y坐标
|
||
pdf.SetXY(currentX+colW, startY)
|
||
currentX += colW
|
||
}
|
||
|
||
return maxHeaderHeight
|
||
}
|
||
|
||
// renderRows 渲染数据行(支持自动分页)
|
||
func (r *DatabaseTableRenderer) renderRows(pdf *gofpdf.Fpdf, rows [][]string, colWidths []float64, lineHt float64, headers []string, headerColWidths []float64) {
|
||
numCols := len(colWidths)
|
||
pdf.SetFillColor(245, 245, 220) // 米色背景
|
||
pdf.SetTextColor(0, 0, 0) // 深黑色文字,确保清晰
|
||
r.fontManager.SetFont(pdf, "", 9)
|
||
|
||
// 获取页面尺寸和边距
|
||
_, pageHeight := pdf.GetPageSize()
|
||
_, _, _, bottomMargin := pdf.GetMargins()
|
||
minSpaceForRow := lineHt * 3 // 至少需要3倍行高的空间
|
||
|
||
validRowIndex := 0 // 用于交替填充的有效行索引
|
||
for rowIndex, row := range rows {
|
||
// 检查这一行是否有有效内容
|
||
hasContent := false
|
||
for _, cell := range row {
|
||
if strings.TrimSpace(cell) != "" {
|
||
hasContent = true
|
||
break
|
||
}
|
||
}
|
||
|
||
// 跳过完全为空的行
|
||
if !hasContent {
|
||
continue
|
||
}
|
||
|
||
// 检查是否需要分页
|
||
currentY := pdf.GetY()
|
||
if currentY+minSpaceForRow > pageHeight-bottomMargin {
|
||
// 需要分页
|
||
r.logger.Debug("表格需要分页",
|
||
zap.Int("row_index", rowIndex),
|
||
zap.Float64("current_y", currentY),
|
||
zap.Float64("page_height", pageHeight))
|
||
pdf.AddPage()
|
||
// 在新页面上重新绘制表头
|
||
if len(headers) > 0 && len(headerColWidths) > 0 {
|
||
newHeaderStartY := pdf.GetY()
|
||
headerHeight := r.renderHeader(pdf, headers, headerColWidths, newHeaderStartY)
|
||
pdf.SetXY(15.0, newHeaderStartY+headerHeight)
|
||
}
|
||
}
|
||
|
||
startY := pdf.GetY()
|
||
fill := (validRowIndex % 2) == 0 // 交替填充
|
||
validRowIndex++
|
||
|
||
// 计算这一行的最大高度
|
||
// 确保lineHt有效
|
||
if lineHt <= 0 {
|
||
lineHt = 5.0 // 默认行高
|
||
}
|
||
maxCellHeight := lineHt * 2.0 // 使用合理的最小高度
|
||
for j := 0; j < numCols && j < len(row); j++ {
|
||
cell := row[j]
|
||
cellWidth := colWidths[j] - 4 // 减少内边距到4mm,给文本更多空间
|
||
|
||
// 先清理单元格文本(在计算宽度之前)
|
||
cell = r.cleanTextForPDF(cell)
|
||
|
||
// 计算文本实际宽度,判断是否需要换行
|
||
textWidth := r.getTextWidth(pdf, cell)
|
||
var lines []string
|
||
|
||
// 只有当文本宽度超过单元格宽度时才换行
|
||
if textWidth > cellWidth {
|
||
// 文本需要换行,使用安全的SplitText方法
|
||
lines = r.safeSplitText(pdf, cell, cellWidth)
|
||
} else {
|
||
// 文本不需要换行,单行显示
|
||
lines = []string{cell}
|
||
}
|
||
|
||
// 确保lines不为空且有效
|
||
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
|
||
lines = []string{cell}
|
||
if lines[0] == "" {
|
||
lines[0] = " " // 至少保留一个空格,避免高度为0
|
||
}
|
||
}
|
||
|
||
// 计算单元格高度,确保不会出现Inf或NaN
|
||
lineCount := float64(len(lines))
|
||
if lineCount <= 0 {
|
||
lineCount = 1
|
||
}
|
||
if lineHt <= 0 {
|
||
lineHt = 5.0 // 默认行高
|
||
}
|
||
cellHeight := lineCount * lineHt
|
||
// 添加上下内边距
|
||
cellHeight += lineHt * 0.8 // 上下各0.4倍行高的内边距
|
||
if cellHeight < lineHt*2.0 {
|
||
cellHeight = lineHt * 2.0
|
||
}
|
||
// 检查是否为有效数值
|
||
if math.IsInf(cellHeight, 0) || math.IsNaN(cellHeight) {
|
||
cellHeight = lineHt * 2.0
|
||
}
|
||
if cellHeight > maxCellHeight {
|
||
maxCellHeight = cellHeight
|
||
}
|
||
}
|
||
|
||
// 再次检查分页(在计算完行高后)
|
||
if startY+maxCellHeight > pageHeight-bottomMargin {
|
||
r.logger.Debug("行高度超出页面,需要分页",
|
||
zap.Int("row_index", rowIndex),
|
||
zap.Float64("row_height", maxCellHeight))
|
||
pdf.AddPage()
|
||
startY = pdf.GetY()
|
||
}
|
||
|
||
// 绘制这一行的所有单元格
|
||
currentX := 15.0
|
||
for j := 0; j < numCols && j < len(row); j++ {
|
||
colW := colWidths[j]
|
||
cell := row[j]
|
||
|
||
// 清理单元格数据,移除任何残留的HTML标签和样式
|
||
cell = r.cleanTextForPDF(cell)
|
||
|
||
// 绘制单元格背景
|
||
if fill {
|
||
pdf.SetFillColor(250, 250, 235) // 稍深的米色
|
||
} else {
|
||
pdf.SetFillColor(255, 255, 255)
|
||
}
|
||
pdf.Rect(currentX, startY, colW, maxCellHeight, "FD")
|
||
|
||
// 绘制单元格文本(只绘制非空内容)
|
||
if cell != "" {
|
||
// 计算文本的实际宽度和单元格可用宽度
|
||
cellWidth := colW - 4
|
||
textWidth := r.getTextWidth(pdf, cell)
|
||
|
||
var textLines []string
|
||
// 只有当文本宽度超过单元格宽度时才换行
|
||
if textWidth > cellWidth {
|
||
// 文本需要换行,使用安全的SplitText方法
|
||
textLines = r.safeSplitText(pdf, cell, cellWidth)
|
||
} else {
|
||
// 文本不需要换行,单行显示
|
||
textLines = []string{cell}
|
||
}
|
||
|
||
textHeight := float64(len(textLines)) * lineHt
|
||
if textHeight < lineHt {
|
||
textHeight = lineHt
|
||
}
|
||
|
||
// 计算垂直居中的Y位置
|
||
cellCenterY := startY + maxCellHeight/2
|
||
textStartY := cellCenterY - textHeight/2
|
||
|
||
// 设置文本位置(水平左对齐,垂直居中,减少左边距)
|
||
pdf.SetXY(currentX+2, textStartY)
|
||
// 再次确保颜色为深黑色(防止被其他设置覆盖)
|
||
pdf.SetTextColor(0, 0, 0)
|
||
// 设置字体,确保颜色不会变淡
|
||
r.fontManager.SetFont(pdf, "", 9)
|
||
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
||
pdf.SetTextColor(0, 0, 0)
|
||
// 安全地渲染文本,使用正常的行高
|
||
func() {
|
||
defer func() {
|
||
if rec := recover(); rec != nil {
|
||
// 静默处理,不记录日志
|
||
}
|
||
}()
|
||
// 使用正常的行高,文本已经垂直居中
|
||
pdf.MultiCell(cellWidth, lineHt, cell, "", "L", false)
|
||
}()
|
||
}
|
||
|
||
// 重置Y坐标
|
||
pdf.SetXY(currentX+colW, startY)
|
||
currentX += colW
|
||
}
|
||
|
||
// 检查maxCellHeight是否为有效数值
|
||
if math.IsInf(maxCellHeight, 0) || math.IsNaN(maxCellHeight) {
|
||
maxCellHeight = lineHt * 2.0
|
||
}
|
||
if maxCellHeight <= 0 {
|
||
maxCellHeight = lineHt * 2.0
|
||
}
|
||
|
||
// 移动到下一行
|
||
nextY := startY + maxCellHeight
|
||
if math.IsInf(nextY, 0) || math.IsNaN(nextY) {
|
||
nextY = pdf.GetY() + lineHt*2.0
|
||
}
|
||
pdf.SetXY(15.0, nextY)
|
||
}
|
||
}
|
||
|
||
// getTextWidth 获取文本宽度
|
||
func (r *DatabaseTableRenderer) getTextWidth(pdf *gofpdf.Fpdf, text string) float64 {
|
||
if r.fontManager.IsChineseFontAvailable() {
|
||
width := pdf.GetStringWidth(text)
|
||
// 如果宽度为0或太小,使用更准确的估算
|
||
if width < 0.1 {
|
||
return r.estimateTextWidth(text)
|
||
}
|
||
return width
|
||
}
|
||
// 估算宽度
|
||
return r.estimateTextWidth(text)
|
||
}
|
||
|
||
// estimateTextWidth 估算文本宽度(处理中英文混合)
|
||
func (r *DatabaseTableRenderer) estimateTextWidth(text string) float64 {
|
||
charCount := 0.0
|
||
for _, r := range text {
|
||
// 中文字符通常比英文字符宽
|
||
if r >= 0x4E00 && r <= 0x9FFF {
|
||
charCount += 1.8 // 中文字符约1.8倍宽度
|
||
} else if r >= 0x3400 && r <= 0x4DBF {
|
||
charCount += 1.8 // 扩展A
|
||
} else if r >= 0x20000 && r <= 0x2A6DF {
|
||
charCount += 1.8 // 扩展B
|
||
} else {
|
||
charCount += 1.0 // 英文字符和数字
|
||
}
|
||
}
|
||
return charCount * 3.0 // 基础宽度3mm
|
||
}
|