443 lines
11 KiB
Go
443 lines
11 KiB
Go
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()
|
||
}
|