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