table
This commit is contained in:
parent
66fffc25ee
commit
a2c7e94394
11
go.mod
11
go.mod
|
@ -1,16 +1,19 @@
|
||||||
module git.u8t.cn/open/goutil
|
module goutil
|
||||||
|
|
||||||
go 1.24
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/fogleman/gg v1.3.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/speps/go-hashids v2.0.0+incompatible
|
github.com/speps/go-hashids v2.0.0+incompatible
|
||||||
gorm.io/gorm v1.30.2
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
golang.org/x/image v0.31.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||||
golang.org/x/text v0.20.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,424 @@
|
||||||
|
package goutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"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 {
|
||||||
|
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 (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 {
|
||||||
|
// 先创建一个临时上下文来测量文本
|
||||||
|
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()
|
||||||
|
}
|
Loading…
Reference in New Issue