Golang生成二维码并合并海报图

项目业务中,有需求要将数据生成到二维码中,再将二维码与背景海报进行合并,当然你还可以获取微信头像嵌入到海报中。

  • 对应产生二维码的三方包:

github.com/skip2/go-qrcode

  • 然后我们就熟练的使用golang官方标准库即可:

    “image” “image/draw” “image/jpeg” “image/png”

代码demo展示:

说明下,你可以直接将背景图放到当前包的目录下或者指定想要读取的目录中,对应最后生成的海报图也是一样的。

而在项目中我们选择两者(即原背景图和最终生成的海报图)都直接放到云储存OSS上进行获取存储。

下面代码会兼容这两种方式~

数据结构如下:

var (
	bgImg     image.Image
	qrCodeImg image.Image
	offset    image.Point
)

type QrCode struct {
	Debug        bool   `json:"debug"`         // 调试模式本地生成海报 不走OSS
	Id           string `json:"id"`            // 海报的唯一标识
	Size         int    `json:"size"`          // 二维码大小 150 == x150 y150
	Type         int    `json:"type"`          // 生成图片类型,默认1.jpg  2.png
	Content      string `json:"content"`       // 二维码识别出的内容
	BackendImage string `json:"backend_image"` // 背景图片名称 3.png
	MidX         bool   `json:"mid_x"`         // 二维码X坐标是否居中
	MidY         bool   `json:"mid_y"`         // 二维码y坐标是否居中
	X            int    `json:"x"`             // 二维码相对图片坐标
	Y            int    `json:"y"`
}	

库初始化:

/*
 * content: 二维码扫码读取的内容
 * BackendImage: debug模式只需要传入背景图片名称,否知传入URL
 * id:业务UID
 * 默认二维码位于背景图位置 X, Y 都是居中位置
 */
func NewQrCodeImage(content, BackendImage, id string) (q *QrCode) {
	if id == "" {
		return
	}
	q = &QrCode{
		Content:      content,
		Size:         150, // 默认150. X:150 y:150
		BackendImage: BackendImage,
		X:            0,
		Y:            0,
		Id:           id,
	}
	q.MiddleX()
	q.MiddleY()
	return
}

func (q *QrCode) SetQrCodeSize(size int) {
	q.Size = size
}

func (q *QrCode) SetX(x int) {
	q.X = x
	q.MidX = false
}

func (q *QrCode) SetY(y int) {
	q.Y = y
	q.MidY = false
}

// 设置debug模式,图片都应该存在本地该路径下 便于直接观察调试图片效果
func (q *QrCode) DebugCode() {
	q.Debug = true
}

func (q *QrCode) MiddleX() {
	q.MidX = true
}

func (q *QrCode) MiddleY() {
	q.MidY = true
}

创建二维码:

推荐使用库: "github.com/skip2/go-qrcode"
// content: 二维码识别信息内容输入
func (q *QrCode) createQrCode() (img image.Image, err error) {
	qrCode, err := qrcode.New(q.Content, qrcode.Highest)
	if err != nil {
		err = errors.New("创建二维码失败")
		return
	}

	qrCode.DisableBorder = true
	img = qrCode.Image(q.Size)
	return
}

读取背景图合成海报:

func readImgData(url string) (pix []byte, file io.ReadCloser, err error) {
	resp, err := http.Get(url)
	if err != nil {
		return
	}
	// defer resp.Body.Close()
	file = resp.Body
	return
}

func (q *QrCode) QrCode4ImageDebug() (err error) {
	nameList := strings.Split(q.BackendImage, ".")
	imageType := nameList[len(nameList)-1]
	qrCodeImg, err = q.createQrCode()
	if err != nil {
		fmt.Println("生成二维码失败:", err)
		return
	}

	i, err := os.Open(path.Base("./" + q.BackendImage))
	if err != nil {
		return
	}
	defer i.Close()
	switch imageType {
	case "png":
		bgImg, err = png.Decode(i)
		if err != nil {
			return
		}
	case "jpg", "jpeg":
		bgImg, err = jpeg.Decode(i)
		if err != nil {
			return
		}
	default:
		err = errors.New("图片格式只支持png/jpg/jpeg")
		return
	}

	b := bgImg.Bounds()
	offset = image.Pt(q.X, q.Y)
	if q.MidX {
		offset = image.Pt(b.Max.X/2-q.Size/2, q.Y)
	}

	if q.MidY {
		offset = image.Pt(q.X, b.Max.Y/2-q.Size/2)
	}

	if q.MidX && q.MidY {
		offset = image.Pt(b.Max.X/2-q.Size/2, b.Max.Y/2-q.Size/2)
	}
	m := image.NewRGBA(b)
	draw.Draw(m, b, bgImg, image.Point{X: 0, Y: 0}, draw.Src)
	draw.Draw(m, qrCodeImg.Bounds().Add(offset), qrCodeImg, image.Point{X: 0, Y: 0}, draw.Over)

	// 本地生成海报图
	nowName := fmt.Sprintf("%s_backend_%s.%s", nameList[0], q.Id, imageType)
	j, err := os.Create(path.Base(nowName))
	if err != nil {
		return
	}
	defer j.Close()
	if nameList[1] == "png" {
		_ = png.Encode(j, m)
	} else {
		_ = jpeg.Encode(j, m, nil)
	}
	return
}

func (q *QrCode) QrCode4Image() (addr string, err error) {
	if q.Debug {
		err = q.QrCode4ImageDebug()
		if err != nil {
			return
		}
		return
	}

	nameList := strings.Split(q.BackendImage, ".")
	imageType := nameList[len(nameList)-1]
	imageHostList := strings.Split(nameList[len(nameList)-2], "/")
	imageHost := imageHostList[len(imageHostList)-1]
	nowName := fmt.Sprintf("%s_backend_%s.%s", imageHost, q.Id, imageType)
	if !oss.IsFilePostfix(nowName) { // 云OSS格式限制判断
		err = errors.New("上传文件格式不符合规范,请重新上传~")
		return
	}

	ossClient := oss.GetClient()             // 初始化你的oss
	exit, _ := ossClient.FileIsExit(nowName) // 判断OSS上该文件是否已存在
	addr = nowName                           // 返回文件名
	if exit {
		return
	}

	_, file, err := readImgData(q.BackendImage) // 读取背景图 URL
	if err != nil {
		return
	}
	defer file.Close()

	qrCodeImg, err = q.createQrCode()
	if err != nil {
		fmt.Println("生成二维码失败:", err)
		return
	}
	switch imageType {
	case "png":
		bgImg, err = png.Decode(file)
		if err != nil {
			return
		}
	case "jpg", "jpeg":
		bgImg, err = jpeg.Decode(file)
		if err != nil {
			return
		}
	default:
		err = errors.New("图片格式只支持png/jpg/jpeg")
		return
	}

	b := bgImg.Bounds()
	offset = image.Pt(q.X, q.Y)
	if q.MidX {
		offset = image.Pt(b.Max.X/2-q.Size/2, q.Y)
	}

	if q.MidY {
		offset = image.Pt(q.X, b.Max.Y/2-q.Size/2)
	}

	if q.MidX && q.MidY {
		offset = image.Pt(b.Max.X/2-q.Size/2, b.Max.Y/2-q.Size/2)
	}
	m := image.NewRGBA(b)
	draw.Draw(m, b, bgImg, image.Point{X: 0, Y: 0}, draw.Src)
	draw.Draw(m, qrCodeImg.Bounds().Add(offset), qrCodeImg, image.Point{X: 0, Y: 0}, draw.Over)

	// 上传至 oss
	imgBuff := bytes.NewBuffer(nil)
	if nameList[1] == "png" {
		_ = png.Encode(imgBuff, m)
	} else {
		_ = jpeg.Encode(imgBuff, m, nil)
	}

	ossName, err := ossClient.Upload(nowName, imgBuff.Bytes())
	log.Error(ossName)
	return
}

调用:

qDebug := NewQrCodeImage("http://h5.ijianqu.com/share/redpack.html?id=ife12z4", "share_red.png", "4444")
	qDebug.DebugCode()
	qDebug.SetY(760)
	addr, err := qDebug.QrCode4Image()
	if err != nil {
		t.Fatal(err)
	}
	t.Log(addr)

完整代码可见地址如下:

https://github.com/driverzhang/golang-qrcode

转折:

由于换了家公司,遇见一个功能要求后端渲染分享图生成分享海报,要求更为复杂数据和模式变的多了些,以上的代码库可能就显得力不从心

这里直接给出 更好的解决方案,换个三方库吧:

github.com/fogleman/gg

没错这个库实现的功能非常丰富,即可切圆形头像,也能任意嵌入图片和数据并且压缩调整你的分享图样式。

实例代码如下:

// DrawCircleImg 头像圆图
func DrawCircleImg(im image.Image) (image.Image, error) {
	loadStart := time.Now()
	defer func() {
		log.Errorf("生成圆头像耗时: %v\n", time.Now().Sub(loadStart).Milliseconds())
	}()
	b := im.Bounds()
	w := float64(b.Dx())
	h := float64(b.Dy())
	dc := gg.NewContext(int(w), int(h))

	r := float64(w / 2) // 半径

	dc.DrawRoundedRectangle(0, 0, w, h, r)
	dc.Clip()
	dc.DrawImage(im, 0, 0)
	return dc.Image(), nil
}


// ScaleImage 缩放图片
func ScaleImage(image image.Image, x, y int) image.Image {
	loadStart := time.Now()
	defer func() {
		log.Errorf("缩放耗时: %v\n", time.Now().Sub(loadStart).Milliseconds())
	}()
	w := image.Bounds().Size().X
	h := image.Bounds().Size().Y
	dc := gg.NewContext(x, y)
	var ax float64 = float64(x) / float64(w)
	var ay float64 = float64(y) / float64(h)
	dc.Scale(ax, ay)
	dc.DrawImage(image, 0, 0)
	return dc.Image()
}
golang