This commit is contained in:
jiangyong 2025-10-06 11:15:41 +08:00
parent 66fffc25ee
commit a2c7e94394
2 changed files with 431 additions and 4 deletions

11
go.mod
View File

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

424
table.go Normal file
View File

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