go根据传入的表头和表内容切片,返回一张table式图片

package table2base64

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/golang/freetype/truetype"
	"golang.org/x/image/font"
	"golang.org/x/image/math/fixed"
	"icode.xxx.com/xx/xx/env"
	"icode.xxx.com/xx/xx-xx-xx/xxx-xx/library/utils"
	"icode.xxx.com/xx/xx-xx-xx/xxx-xx/library/utils/shence"
)

const (
	DefaultClosWidth      int = 103 //默认列宽
	DefaultRowsHeight     int = 36  //默认行高
	DefaultTextRowsHeight int = 20  //默认文字描述行高
)

var (
	DefaultTTFFile     = ""
	DefaultTTFFileBOLD = ""
)

// 初始化函数
func init() {
	DefaultTTFFile = env.ConfDir() + "/fonts" + "/AlibabaPuHuiTi-3-55-Regular/AlibabaPuHuiTi-3-55-Regular.ttf"
	DefaultTTFFileBOLD = env.ConfDir() + "/fonts" + "/AlibabaPuHuiTi-3-85-Bold/AlibabaPuHuiTi-3-85-Bold.ttf"
	if !utils.FileExists(DefaultTTFFileBOLD) {
		utils.DownloadFile("https://puhuiti.oss-cn-hangzhou.aliyuncs.com/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-85-Bold.zip", env.ConfDir()+"/fonts")
	}
	if !utils.FileExists(DefaultTTFFile) {
		utils.DownloadFile("https://puhuiti.oss-cn-hangzhou.aliyuncs.com/AlibabaPuHuiTi-3/AlibabaPuHuiTi-3-55-Regular.zip", env.ConfDir()+"/fonts")
	}
}

type DrawString interface {
	string() string
}

// TabString 自定义字符串类型
type TabString string

// string 方法返回字符串 s 的值
func (s TabString) string() string {
	return string(s)
}

// TabNumber 自定义数值类型
type TabNumber float64

// string 方法返回 TabNumber 类型的字符串表示形式
func (n TabNumber) string() string {
	return strconv.FormatFloat(float64(n), 'f', -1, 64)
}

type TableContent [][]DrawString
type TableImg struct {
	TTFFile        string               //字体文件路径
	TTFFILEBold    string               //字体文件路径(粗体)
	Header         []string             //表格头
	Content        [][]interface{}      //外部初始化表格内容
	TextDesc       []string             //文字描述
	ClosWidth      int                  //列宽
	RowsHeight     int                  //行高
	TextRowsHeight int                  //文字描述行高
	ShenCeData     []*shence.ReportData //神策数据

	selfContent TableContent //内部使用表格内容
	imgWidth    int          //图片宽
	tableHeight int          //表格高
	imgHeight   int          //图片高
	cols        int          //列数
	rows        int          //行数
	base64Str   string       //base64数据
	img         *image.RGBA  //图像资源对象

	shenCeBase64Str string      //神策数据base64
	shenCeImg       *image.RGBA //神策数据图像资源对象
}

// NewTableImg 返回一个TableImg对象
func NewTableImg(tab *TableImg) *TableImg {
	var c TableContent
	for _, con := range tab.Content {
		var cSlice []DrawString
		for _, cc := range con {
			value := convertToDrawValue(cc)
			cSlice = append(cSlice, value)
		}
		c = append(c, cSlice)
	}
	if tab.TTFFile == "" {
		tab.TTFFile = DefaultTTFFile
	}
	if tab.TTFFILEBold == "" {
		tab.TTFFILEBold = DefaultTTFFileBOLD
	}
	return &TableImg{
		TTFFile:        tab.TTFFile,
		TTFFILEBold:    tab.TTFFILEBold,
		Header:         tab.Header,
		Content:        tab.Content,
		ClosWidth:      tab.ClosWidth,
		RowsHeight:     tab.RowsHeight,
		TextDesc:       tab.TextDesc,
		TextRowsHeight: tab.TextRowsHeight,
		ShenCeData:     tab.ShenCeData,

		selfContent:     c,
		imgWidth:        tab.ClosWidth * len(tab.Header),
		tableHeight:     tab.RowsHeight * (len(tab.Content) + 1),
		imgHeight:       tab.RowsHeight*(len(tab.Content)+1) + tab.TextRowsHeight*(len(tab.TextDesc)+1),
		cols:            len(tab.Header),
		rows:            len(tab.Content) + 1,
		base64Str:       "",
		img:             nil,
		shenCeBase64Str: "",
		shenCeImg:       nil,
	}
}

// convertToDrawValue 将一个输入转换为DrawString类型的值
func convertToDrawValue(cc interface{}) DrawString {
	var value DrawString
	switch v := cc.(type) {
	case int:
		value = TabNumber(v)
	case int8:
		value = TabNumber(v)
	case int16:
		value = TabNumber(v)
	case int32:
		value = TabNumber(v)
	case int64:
		value = TabNumber(v)
	case float32:
		value = TabNumber(v)
	case float64:
		value = TabNumber(v)
	case string:
		value = TabString(v)
	default:
		value = TabString("")
	}
	return value
}

// GenerateShenCeImageGetBase64 生成神策数据图片并返回base64
func (tab *TableImg) GenerateShenCeImageGetBase64() (string, error) {
	marginx, marginy := 15, 20
	height := tab.RowsHeight*7 + marginy
	cellWidth, cellHeight := 320, tab.RowsHeight*4+marginy

	tab.shenCeImg = image.NewRGBA(image.Rect(0, 0, tab.imgWidth, height))

	// 以白色作为背景绘制整个图片
	draw.Draw(tab.shenCeImg, tab.shenCeImg.Bounds(), &image.Uniform{color.Gray16{0xf5f8}}, image.Point{}, draw.Src)

	f, err := os.OpenFile(tab.TTFFILEBold, os.O_RDONLY, 0644) // 0644表示文件权限
	if err != nil {
		return "", err
	}
	defer f.Close()
	byteFile, _ := io.ReadAll(f)

	for k, v := range tab.ShenCeData {
		x := marginx + k*(cellWidth+marginx)
		y := marginy * 2
		x1 := x + cellWidth
		y1 := y + cellHeight

		// 创建格子的矩形区域并进行绘制
		fillArea := image.Rect(x, y, x1, y1)
		draw.Draw(tab.shenCeImg, fillArea, &image.Uniform{color.White}, image.Point{0, 0}, draw.Src)

		if _, err := tab.drawText(v.Name, x+10, y+20, byteFile, tab.shenCeImg, 18, color.RGBA{A: 255}); err != nil {
			return "", err
		}
		col := color.RGBA{0, 0, 0, 255}
		if _, err := tab.drawText(fmt.Sprintf("%.0f", v.BaseNumber), x+10, y+20+tab.RowsHeight, byteFile, tab.shenCeImg, 18, col); err != nil {
			return "", err
		}

		col = color.RGBA{255, 0, 0, 255}
		hb := fmt.Sprintf("环比%.2f%%", v.MonthOnMonth)
		if v.MonthOnMonth > 0 {
			col = color.RGBA{25, 95, 31, 255}
			hb = fmt.Sprintf("环比+%.2f%%", v.MonthOnMonth)
		}
		if _, err := tab.drawText(hb, x+10, y+20+tab.RowsHeight*2, byteFile, tab.shenCeImg, 18, col); err != nil {
			return "", err
		}

		col = color.RGBA{255, 0, 0, 255}
		tb := fmt.Sprintf("同比%.2f%%", v.YearOnYear)
		if v.YearOnYear > 0 {
			col = color.RGBA{25, 95, 31, 255}
			tb = fmt.Sprintf("同比+%.2f%%", v.YearOnYear)
		}
		if _, err := tab.drawText(tb, x+10, y+20+tab.RowsHeight*3, byteFile, tab.shenCeImg, 18, col); err != nil {
			return "", err
		}
	}
	if _, err := tab.drawText("产品大盘增长数据:", marginx, marginy, byteFile, tab.shenCeImg, 18, color.RGBA{A: 255}); err != nil {
		return "", err
	}
	if _, err := tab.drawText("投放大盘数据:", marginx, marginy*3+cellHeight, byteFile, tab.shenCeImg, 18, color.RGBA{A: 255}); err != nil {
		return "", err
	}

	// 创建一个内存缓冲区
	var buf bytes.Buffer
	// 将图片编码为 PNG 格式并写入缓冲区
	err = png.Encode(&buf, tab.shenCeImg)
	if err != nil {
		return "", err
	}
	// 将 PNG 图片转换为 base64 编码的字符串
	tab.shenCeBase64Str = base64.StdEncoding.EncodeToString(buf.Bytes())
	return tab.shenCeBase64Str, nil
}

// GenerateImageGetBase64 生成表格图片并返回base64
func (tab *TableImg) GenerateImageGetBase64() (string, error) {
	cols := len(tab.Header)
	rows := len(tab.Content) + 1
	// 创建一个新的 RGBA 图片
	tab.img = image.NewRGBA(image.Rect(0, 0, tab.imgWidth, tab.imgHeight))

	// 以白色作为背景绘制整个图片
	draw.Draw(tab.img, tab.img.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)

	downMovePx := tab.imgHeight - tab.tableHeight
	//填充表头背景色
	fillArea := image.Rect(0, 0+downMovePx, tab.imgWidth, tab.RowsHeight+downMovePx)
	draw.Draw(tab.img, fillArea, &image.Uniform{color.RGBA{162, 206, 232, 255}}, image.Point{0, 0}, draw.Src)

	yesterDay := time.Now().AddDate(0, 0, -1).Format("20060102")
	for i, v := range tab.selfContent {
		i += 1
		if strings.Contains(v[2].string(), "汇总") || strings.Contains(v[3].string(), "汇总") {
			fillArea := image.Rect(0, tab.RowsHeight*i+downMovePx, tab.imgWidth, tab.RowsHeight*(i+1)+downMovePx)
			draw.Draw(tab.img, fillArea, &image.Uniform{color.RGBA{204, 116, 79, 255}}, image.Point{0, 0}, draw.Src)
			continue
		}
		if v[0].string() == "汇总" {
			fillArea := image.Rect(0, tab.RowsHeight*(i)+downMovePx, tab.imgWidth, tab.RowsHeight*(i+1)+downMovePx)
			draw.Draw(tab.img, fillArea, &image.Uniform{color.RGBA{192, 236, 252, 255}}, image.Point{0, 0}, draw.Src)
			continue
		}
		//如果是昨天,则填充特殊背景色
		if v[0].string() == yesterDay {
			fillArea := image.Rect(0, tab.RowsHeight*i+downMovePx, tab.imgWidth, tab.RowsHeight*(i+1)+downMovePx)
			draw.Draw(tab.img, fillArea, &image.Uniform{color.RGBA{233, 204, 206, 255}}, image.Point{0, 0}, draw.Src)
		}
	}

	//绘制表格线
	for i := 0; i <= cols; i++ {
		x := i * (tab.imgWidth / cols)
		if x >= tab.imgWidth {
			x = tab.imgWidth - 1
		}
		tab.drawLine(x, 0+downMovePx, x, tab.tableHeight+downMovePx, color.RGBA{123, 123, 123, 255})
	}
	for i := 0; i <= rows; i++ {
		y := i*(tab.tableHeight/rows) + downMovePx
		if y >= tab.imgHeight {
			y = tab.imgHeight - 1
		}
		tab.drawLine(0, y, tab.imgWidth, y, color.RGBA{123, 123, 123, 255})
	}

	f, err := os.OpenFile(tab.TTFFile, os.O_RDONLY, 0644) // 0644表示文件权限
	if err != nil {
		return "", err
	}
	defer f.Close()
	byteFile, _ := io.ReadAll(f)

	fBold, err := os.OpenFile(tab.TTFFILEBold, os.O_RDONLY, 0644) // 0644表示文件权限
	if err != nil {
		return "", err
	}
	defer fBold.Close()
	byteFileBold, _ := io.ReadAll(fBold)
	// 在图片上绘制内容
	// 绘制 header 中的内容
	for i, h := range tab.Header {
		if _, err := tab.drawText(h, (tab.imgWidth/cols)*i+10, 20+downMovePx, byteFileBold, tab.img, 14, color.RGBA{A: 255}); err != nil {
			return "", err
		}

	}
	textGreen := color.RGBA{0, 128, 0, 255} // 绿色
	textRed := color.RGBA{255, 0, 0, 255}   // 红色
	// 绘制 content 中的内容
	for i, c := range tab.selfContent {
		bFile := byteFile
		if c[0].string() == yesterDay || c[0].string() == "汇总" || strings.Contains(c[2].string(), "汇总") || strings.Contains(c[3].string(), "汇总") {
			bFile = byteFileBold
		}
		y := (tab.tableHeight/rows)*(i+1) + 20 + downMovePx
		n := 0
		for j, item := range c {
			x := (tab.imgWidth/cols)*j + 10
			textColor := color.RGBA{A: 255}
			if strings.HasPrefix(item.string(), "+") {
				n++
				textColor = textGreen // 绿色
				if n%2 == 0 {         //CPI 的颜色和其他的是相反的
					textColor = textRed // 红色
				}
			} else if item.string() != "-" && strings.HasPrefix(item.string(), "-") {
				n++
				textColor = textRed // 红色
				if n%2 == 0 {       //CPI 的颜色和其他的是相反的
					textColor = textGreen // 绿色
				}
			}
			if _, err := tab.drawText(item.string(), x, y, bFile, tab.img, 14, textColor); err != nil {
				return "", err
			}
		}
	}
	// 绘制 文字描述 中的内容
	for i, c := range tab.TextDesc {
		y := 20
		y += i * tab.TextRowsHeight
		x := 10
		j := 0
		for _, word := range strings.Split(c, " ") {
			col := color.RGBA{0, 0, 0, 255}
			if len(word) == 0 {
				continue
			}
			if word[0] == '+' {
				j++
				if j%2 == 1 { //如果是激活,+用绿,如果是CPI,+用红
					col = textGreen // 绿色
				} else {
					col = textRed // 红色
				}
			} else if word[0] == '-' {
				j++
				if j%2 == 1 { //如果是激活,-用红,如果是CPI,-用绿
					col = textRed // 红色
				} else {
					col = textGreen // 绿色
				}
			}
			width, err := tab.drawText(word, x, y, byteFileBold, tab.img, 14, col)
			if err != nil {
				return "", err
			}
			x += width
		}
	}

	// 创建一个内存缓冲区
	var buf bytes.Buffer
	// 将图片编码为 PNG 格式并写入缓冲区
	err = png.Encode(&buf, tab.img)
	if err != nil {
		return "", err
	}
	// 将 PNG 图片转换为 base64 编码的字符串
	tab.base64Str = base64.StdEncoding.EncodeToString(buf.Bytes())

	return tab.base64Str, nil
}

// drawText 方法绘制文本到图像上
func (tab *TableImg) drawText(text string, x, y int, fileByte []byte, img *image.RGBA, size float64, col color.RGBA) (width int, err error) {
	point := image.Point{X: x, Y: y}
	ff, _ := truetype.Parse(fileByte)
	face := truetype.NewFace(ff, &truetype.Options{
		Size: size,
	})
	drawer := &font.Drawer{
		Dst:  img,
		Src:  image.NewUniform(col),
		Face: face,
		Dot:  fixed.P(point.X, point.Y),
	}
	drawer.DrawString(text)
	options := &font.Drawer{
		Face: face,
	}
	options.Dot = fixed.P(0, 0)
	return fixed.Int26_6(options.MeasureString(text)).Ceil(), nil
}

// drawLine 函数绘制一条直线。
// x0,y0,x1和y1表示起点和终点的坐标值,c为直线颜色。
func (tab *TableImg) drawLine(x0, y0, x1, y1 int, c color.Color) {
	dx := abs(x1 - x0)
	dy := abs(y1 - y0)
	sx, sy := 1, 1
	if x0 > x1 {
		sx = -1
	}
	if y0 > y1 {
		sy = -1
	}
	err := dx - dy

	for {
		tab.img.Set(x0, y0, c)
		if x0 == x1 && y0 == y1 {
			break
		}
		e2 := 2 * err
		if e2 > -dy {
			err -= dy
			x0 += sx
		}
		if e2 < dx {
			err += dx
			y0 += sy
		}
	}
}

// abs 函数返回输入整数的绝对值。
func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值