diff --git a/go.mod b/go.mod index 30b9737..61a1570 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,19 @@ -module git.u8t.cn/open/goutil +module goutil -go 1.24 +go 1.25 require ( + github.com/fogleman/gg v1.3.0 github.com/sirupsen/logrus v1.9.3 github.com/speps/go-hashids v2.0.0+incompatible - gorm.io/gorm v1.30.2 + gorm.io/gorm v1.31.0 ) require ( + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/jinzhu/inflection v1.0.0 // 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/text v0.20.0 // indirect + golang.org/x/text v0.29.0 // indirect ) diff --git a/table.go b/table.go new file mode 100644 index 0000000..93ae109 --- /dev/null +++ b/table.go @@ -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() +}