goutil/table.go

443 lines
11 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 goutil
import (
"fmt"
"image/color"
"math"
"sync"
"github.com/fogleman/gg"
)
type TableData struct {
Title string
Headers []string
Rows [][]string
}
type TableConfig struct {
Padding float64
FontSize float64
ScaleFactor float64 // 新增:缩放因子,用于高清输出
HeaderBg color.Color
RowBgEven color.Color
RowBgOdd color.Color
TextColor color.Color
HeaderTextColor color.Color
BorderColor color.Color
LineColor color.Color
MinColWidth float64
MaxColWidth float64
FontPath string
LineWidth float64 // 新增:线条宽度
BorderWidth float64 // 新增:边框宽度
// 新增:标题相关配置
TitleFontSize float64
TitleColor color.Color
TitleHeight float64
TitleTopMargin float64
}
type TableGenerator struct {
lock sync.RWMutex
config *TableConfig
dc *gg.Context
}
func NewTableGenerator(config *TableConfig) *TableGenerator {
if config == nil {
config = &TableConfig{}
}
if config.FontPath == "" {
config.FontPath = "/app/conf/STHeiti_Light.ttc"
}
// 设置默认配置
if config.Padding == 0 {
config.Padding = 12
}
if config.FontSize == 0 {
config.FontSize = 14
}
if config.ScaleFactor == 0 {
config.ScaleFactor = 2.0 // 默认2倍缩放生成高清图片
}
if config.HeaderBg == nil {
config.HeaderBg = color.RGBA{R: 70, G: 130, B: 180, A: 255}
}
if config.RowBgEven == nil {
config.RowBgEven = color.White
}
if config.RowBgOdd == nil {
config.RowBgOdd = color.RGBA{R: 248, G: 248, B: 248, A: 255}
}
if config.TextColor == nil {
config.TextColor = color.Black
}
if config.HeaderTextColor == nil {
config.HeaderTextColor = color.White
}
if config.BorderColor == nil {
config.BorderColor = color.Black
}
if config.LineColor == nil {
config.LineColor = color.Gray{Y: 180}
}
if config.MinColWidth == 0 {
config.MinColWidth = 80
}
if config.MaxColWidth == 0 {
config.MaxColWidth = 300
}
if config.LineWidth == 0 {
config.LineWidth = 1.0
}
if config.BorderWidth == 0 {
config.BorderWidth = 2.0
}
// 新增:标题默认配置
if config.TitleFontSize == 0 {
config.TitleFontSize = 20 // 标题字体比正文大
}
if config.TitleColor == nil {
config.TitleColor = color.RGBA{R: 0, G: 0, B: 0, A: 255} // 黑色标题
}
if config.TitleHeight == 0 {
config.TitleHeight = 20 // 标题区域高度
}
if config.TitleTopMargin == 0 {
config.TitleTopMargin = 20 // 标题顶部边距
}
return &TableGenerator{config: config}
}
func (t *TableData) Check() error {
headerLen := len(t.Headers)
for _, row := range t.Rows {
if headerLen != len(row) {
return fmt.Errorf("header length does not match row length")
}
}
return nil
}
// 应用缩放因子
func (g *TableGenerator) scaled(value float64) float64 {
return value * g.config.ScaleFactor
}
// 计算每列的最大宽度(考虑缩放)
func (g *TableGenerator) calculateColumnWidths(data *TableData) []float64 {
colCount := len(data.Headers)
colWidths := make([]float64, colCount)
// 初始化列宽为最小宽度(考虑缩放)
for i := range colWidths {
colWidths[i] = g.scaled(g.config.MinColWidth)
}
// 检查表头宽度
for j, header := range data.Headers {
width, _ := g.dc.MeasureString(header)
totalWidth := width + g.scaled(g.config.Padding)*2
if totalWidth > colWidths[j] {
colWidths[j] = math.Min(totalWidth, g.scaled(g.config.MaxColWidth))
}
}
// 检查每行内容的宽度
for _, row := range data.Rows {
for j, cell := range row {
width, _ := g.dc.MeasureString(cell)
totalWidth := width + g.scaled(g.config.Padding)*2
if totalWidth > colWidths[j] {
colWidths[j] = math.Min(totalWidth, g.scaled(g.config.MaxColWidth))
}
}
}
return colWidths
}
// 计算每行的最大高度(考虑缩放)
func (g *TableGenerator) calculateRowHeights(data *TableData, colWidths []float64) []float64 {
rowCount := len(data.Rows) + 1 // 包括表头
rowHeights := make([]float64, rowCount)
// 计算表头高度
_, headerHeight := g.dc.MeasureString("Hg")
rowHeights[0] = headerHeight + g.scaled(g.config.Padding)*2
// 计算每行内容的高度
for i, row := range data.Rows {
maxHeight := 0.0
for j, cell := range row {
// 测量文本在限定宽度内的高度
lines := g.wrapText(cell, colWidths[j]-g.scaled(g.config.Padding)*2)
lineHeight := g.getLineHeight()
cellHeight := float64(len(lines))*lineHeight + g.scaled(g.config.Padding)*2
if cellHeight > maxHeight {
maxHeight = cellHeight
}
}
rowHeights[i+1] = maxHeight
}
return rowHeights
}
// 文本换行处理
func (g *TableGenerator) wrapText(text string, maxWidth float64) []string {
if maxWidth <= 0 {
return []string{text}
}
var lines []string
var currentLine string
// 简单的按字符换行
for _, char := range text {
testLine := currentLine + string(char)
width, _ := g.dc.MeasureString(testLine)
if width <= maxWidth {
currentLine = testLine
} else {
if currentLine != "" {
lines = append(lines, currentLine)
}
currentLine = string(char)
}
}
if currentLine != "" {
lines = append(lines, currentLine)
}
// 如果单行就超过最大宽度,强制截断
if len(lines) == 0 {
lines = []string{text}
}
return lines
}
// 获取行高
func (g *TableGenerator) getLineHeight() float64 {
_, height := g.dc.MeasureString("Hg")
return height * 1.2 // 增加行间距
}
// 计算表格总尺寸
func (g *TableGenerator) calculateTableSize(colWidths []float64, rowHeights []float64) (float64, float64) {
totalWidth := 0.0
for _, width := range colWidths {
totalWidth += width
}
totalHeight := 0.0
for _, height := range rowHeights {
totalHeight += height
}
return totalWidth, totalHeight
}
// 加载高清字体
func (g *TableGenerator) loadHDFont(dc *gg.Context, fontSize float64) error {
scaledFontSize := fontSize * g.config.ScaleFactor
if err := dc.LoadFontFace(g.config.FontPath, scaledFontSize); err != nil {
return fmt.Errorf("无法加载字体 %s (大小: %.1f): %v", g.config.FontPath, scaledFontSize, err)
}
return nil
}
// 绘制标题
func (g *TableGenerator) drawTitle(dc *gg.Context, margin, tableWidth float64, title string) float64 {
if title == "" {
return 0
}
// 保存当前字体设置
originalFontSize := g.config.FontSize
// 设置标题字体
if err := g.loadHDFont(dc, g.config.TitleFontSize); err != nil {
// 如果标题字体加载失败,使用默认字体
g.loadHDFont(dc, originalFontSize)
}
// 计算标题区域高度
titleHeight := g.scaled(g.config.TitleHeight)
titleTopMargin := g.scaled(g.config.TitleTopMargin)
// 绘制标题文字(白色背景,直接绘制在画布上)
dc.SetColor(g.config.TitleColor)
titleWidth, titleTextHeight := dc.MeasureString(title)
titleX := margin + (tableWidth-titleWidth)/2 // 水平居中
// 标题位置,稍微向下一点
titleY := titleTopMargin + titleTextHeight + 5
dc.DrawString(title, titleX, titleY)
// 恢复原来的字体设置
g.loadHDFont(dc, originalFontSize)
return titleHeight
}
// 生成自适应表格(高清版本)
func (g *TableGenerator) Generate(data *TableData, filename string) error {
err := data.Check()
if err != nil {
return err
}
g.lock.Lock()
defer g.lock.Unlock()
// 先创建一个临时上下文来测量文本
tempDC := gg.NewContext(1, 1)
if err := g.loadHDFont(tempDC, g.config.FontSize); err != nil {
return err
}
g.dc = tempDC
// 计算列宽和行高
colWidths := g.calculateColumnWidths(data)
rowHeights := g.calculateRowHeights(data, colWidths)
// 计算表格总尺寸
tableWidth, tableHeight := g.calculateTableSize(colWidths, rowHeights)
// 创建实际绘图上下文(增加边距,考虑缩放和标题高度)
margin := g.scaled(20)
titleHeight := g.scaled(g.config.TitleHeight)
// 计算总高度(包括标题)
totalHeight := tableHeight
if data.Title != "" {
totalHeight += titleHeight + g.scaled(10) // 标题高度加上标题与表格的间距
}
canvasWidth := int(tableWidth + margin*2)
canvasHeight := int(totalHeight + margin*2)
dc := gg.NewContext(canvasWidth, canvasHeight)
// 加载字体到实际上下文
if err := g.loadHDFont(dc, g.config.FontSize); err != nil {
return err
}
g.dc = dc
// 设置高质量抗锯齿
dc.SetLineJoin(gg.LineJoinRound)
dc.SetLineCap(gg.LineCapRound)
// 设置白色背景
dc.SetColor(color.White)
dc.Clear()
// 绘制标题(如果有)
tableStartY := margin
if data.Title != "" {
titleHeight := g.drawTitle(dc, margin, tableWidth, data.Title)
// 表格从标题下方开始
tableStartY = margin + titleHeight + g.scaled(10)
}
// 绘制表格内容
currentY := tableStartY
for i := 0; i < len(rowHeights); i++ {
currentX := margin
rowHeight := rowHeights[i]
for j := 0; j < len(colWidths); j++ {
colWidth := colWidths[j]
// 设置单元格背景色
if i == 0 {
dc.SetColor(g.config.HeaderBg)
} else if i%2 == 0 {
dc.SetColor(g.config.RowBgEven)
} else {
dc.SetColor(g.config.RowBgOdd)
}
dc.DrawRectangle(currentX, currentY, colWidth, rowHeight)
dc.Fill()
// 绘制单元格内容
var cellText string
if i == 0 {
cellText = data.Headers[j]
dc.SetColor(g.config.HeaderTextColor)
} else {
cellText = data.Rows[i-1][j]
dc.SetColor(g.config.TextColor)
}
// 文本换行和绘制
lines := g.wrapText(cellText, colWidth-g.scaled(g.config.Padding)*2)
lineHeight := g.getLineHeight()
// 计算文本起始Y位置垂直居中
totalTextHeight := float64(len(lines)) * lineHeight
textStartY := currentY + (rowHeight-totalTextHeight)/2 + lineHeight*0.8
for lineIndex, line := range lines {
textWidth, _ := dc.MeasureString(line)
textX := currentX + (colWidth-textWidth)/2
textY := textStartY + float64(lineIndex)*lineHeight
dc.DrawString(line, textX, textY)
}
currentX += colWidth
}
currentY += rowHeight
}
// 绘制网格线(使用缩放后的线宽)
g.drawGridLines(dc, colWidths, rowHeights, margin, tableStartY, tableWidth, tableHeight)
// 绘制外边框(使用缩放后的边框宽度)
g.drawOuterBorder(dc, margin, tableStartY, tableWidth, tableHeight)
return dc.SavePNG(filename)
}
// 绘制网格线(高清优化)
func (g *TableGenerator) drawGridLines(dc *gg.Context, colWidths, rowHeights []float64, margin, startY, tableWidth, tableHeight float64) {
dc.SetColor(g.config.LineColor)
dc.SetLineWidth(g.scaled(g.config.LineWidth))
// 绘制水平线
currentY := startY
for i := 0; i <= len(rowHeights); i++ {
dc.DrawLine(margin, currentY, margin+tableWidth, currentY)
dc.Stroke()
if i < len(rowHeights) {
currentY += rowHeights[i]
}
}
// 绘制垂直线
currentX := margin
for j := 0; j <= len(colWidths); j++ {
dc.DrawLine(currentX, startY, currentX, startY+tableHeight)
dc.Stroke()
if j < len(colWidths) {
currentX += colWidths[j]
}
}
}
// 绘制外边框(高清优化)
func (g *TableGenerator) drawOuterBorder(dc *gg.Context, margin, startY, tableWidth, tableHeight float64) {
dc.SetColor(g.config.BorderColor)
dc.SetLineWidth(g.scaled(g.config.BorderWidth))
dc.DrawRectangle(margin, startY, tableWidth, tableHeight)
dc.Stroke()
}