3.15 生成二维码、合并海报

知识点

    图片生成
    二维码生成

本文目标

在文章的详情页中,我们常常会需要去宣传它,而目前最常见的就是发海报了,今天我们将实现如下功能:
    生成二维码
    合并海报(背景图 + 二维码)

实现

首先,你需要在 App 配置项中增加二维码及其海报的存储路径,我们约定配置项名称为 QrCodeSavePath,值为 qrcode/,经过多节连载的你应该能够完成,若有不懂可参照 go-gin-example

生成二维码

安装

1
$ go get -u github.com/boombuler/barcode
Copied!

工具包

考虑生成二维码这一动作贴合工具包的定义,且有公用的可能性,新建 pkg/qrcode/qrcode.go 文件,写入内容:
1
package qrcode
2
3
import (
4
"image/jpeg"
5
6
"github.com/boombuler/barcode"
7
"github.com/boombuler/barcode/qr"
8
9
"github.com/EDDYCJY/go-gin-example/pkg/file"
10
"github.com/EDDYCJY/go-gin-example/pkg/setting"
11
"github.com/EDDYCJY/go-gin-example/pkg/util"
12
)
13
14
type QrCode struct {
15
URL string
16
Width int
17
Height int
18
Ext string
19
Level qr.ErrorCorrectionLevel
20
Mode qr.Encoding
21
}
22
23
const (
24
EXT_JPG = ".jpg"
25
)
26
27
func NewQrCode(url string, width, height int, level qr.ErrorCorrectionLevel, mode qr.Encoding) *QrCode {
28
return &QrCode{
29
URL: url,
30
Width: width,
31
Height: height,
32
Level: level,
33
Mode: mode,
34
Ext: EXT_JPG,
35
}
36
}
37
38
func GetQrCodePath() string {
39
return setting.AppSetting.QrCodeSavePath
40
}
41
42
func GetQrCodeFullPath() string {
43
return setting.AppSetting.RuntimeRootPath + setting.AppSetting.QrCodeSavePath
44
}
45
46
func GetQrCodeFullUrl(name string) string {
47
return setting.AppSetting.PrefixUrl + "/" + GetQrCodePath() + name
48
}
49
50
func GetQrCodeFileName(value string) string {
51
return util.EncodeMD5(value)
52
}
53
54
func (q *QrCode) GetQrCodeExt() string {
55
return q.Ext
56
}
57
58
func (q *QrCode) CheckEncode(path string) bool {
59
src := path + GetQrCodeFileName(q.URL) + q.GetQrCodeExt()
60
if file.CheckNotExist(src) == true {
61
return false
62
}
63
64
return true
65
}
66
67
func (q *QrCode) Encode(path string) (string, string, error) {
68
name := GetQrCodeFileName(q.URL) + q.GetQrCodeExt()
69
src := path + name
70
if file.CheckNotExist(src) == true {
71
code, err := qr.Encode(q.URL, q.Level, q.Mode)
72
if err != nil {
73
return "", "", err
74
}
75
76
code, err = barcode.Scale(code, q.Width, q.Height)
77
if err != nil {
78
return "", "", err
79
}
80
81
f, err := file.MustOpen(name, path)
82
if err != nil {
83
return "", "", err
84
}
85
defer f.Close()
86
87
err = jpeg.Encode(f, code, nil)
88
if err != nil {
89
return "", "", err
90
}
91
}
92
93
return name, path, nil
94
}
Copied!
这里主要聚焦 func (q *QrCode) Encode 方法,做了如下事情:
    获取二维码生成路径
    创建二维码
    缩放二维码到指定大小
    新建存放二维码图片的文件
    将图像(二维码)以 JPEG 4:2:0 基线格式写入文件
另外在 jpeg.Encode(f, code, nil) 中,第三个参数可设置其图像质量,默认值为 75
1
// DefaultQuality is the default quality encoding parameter.
2
const DefaultQuality = 75
3
4
// Options are the encoding parameters.
5
// Quality ranges from 1 to 100 inclusive, higher is better.
6
type Options struct {
7
Quality int
8
}
Copied!

路由方法

1、第一步
在 routers/api/v1/article.go 新增 GenerateArticlePoster 方法用于接口开发
2、第二步
在 routers/router.go 的 apiv1 中新增 apiv1.POST("/articles/poster/generate", v1.GenerateArticlePoster) 路由
3、第三步
修改 GenerateArticlePoster 方法,编写对应的生成逻辑,如下:
1
const (
2
QRCODE_URL = "https://github.com/EDDYCJY/blog#gin%E7%B3%BB%E5%88%97%E7%9B%AE%E5%BD%95"
3
)
4
5
func GenerateArticlePoster(c *gin.Context) {
6
appG := app.Gin{c}
7
qrc := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto)
8
path := qrcode.GetQrCodeFullPath()
9
_, _, err := qrc.Encode(path)
10
if err != nil {
11
appG.Response(http.StatusOK, e.ERROR, nil)
12
return
13
}
14
15
appG.Response(http.StatusOK, e.SUCCESS, nil)
16
}
Copied!

验证

通过 POST 方法访问 http://127.0.0.1:8000/api/v1/articles/poster/generate?token=$token(注意 $token)
image
通过检查两个点确定功能是否正常,如下:
1、访问结果是否 200
2、本地目录是否成功生成二维码图片
image

合并海报

在这一节,将实现二维码图片与背景图合并成新的一张图,可用于常见的宣传海报等业务场景

背景图

image
将背景图另存为 runtime/qrcode/bg.jpg(实际应用,可存在 OSS 或其他地方)

service 方法

打开 service/article_service 目录,新建 article_poster.go 文件,写入内容:
1
package article_service
2
3
import (
4
"image"
5
"image/draw"
6
"image/jpeg"
7
"os"
8
9
"github.com/EDDYCJY/go-gin-example/pkg/file"
10
"github.com/EDDYCJY/go-gin-example/pkg/qrcode"
11
)
12
13
type ArticlePoster struct {
14
PosterName string
15
*Article
16
Qr *qrcode.QrCode
17
}
18
19
func NewArticlePoster(posterName string, article *Article, qr *qrcode.QrCode) *ArticlePoster {
20
return &ArticlePoster{
21
PosterName: posterName,
22
Article: article,
23
Qr: qr,
24
}
25
}
26
27
func GetPosterFlag() string {
28
return "poster"
29
}
30
31
func (a *ArticlePoster) CheckMergedImage(path string) bool {
32
if file.CheckNotExist(path+a.PosterName) == true {
33
return false
34
}
35
36
return true
37
}
38
39
func (a *ArticlePoster) OpenMergedImage(path string) (*os.File, error) {
40
f, err := file.MustOpen(a.PosterName, path)
41
if err != nil {
42
return nil, err
43
}
44
45
return f, nil
46
}
47
48
type ArticlePosterBg struct {
49
Name string
50
*ArticlePoster
51
*Rect
52
*Pt
53
}
54
55
type Rect struct {
56
Name string
57
X0 int
58
Y0 int
59
X1 int
60
Y1 int
61
}
62
63
type Pt struct {
64
X int
65
Y int
66
}
67
68
func NewArticlePosterBg(name string, ap *ArticlePoster, rect *Rect, pt *Pt) *ArticlePosterBg {
69
return &ArticlePosterBg{
70
Name: name,
71
ArticlePoster: ap,
72
Rect: rect,
73
Pt: pt,
74
}
75
}
76
77
func (a *ArticlePosterBg) Generate() (string, string, error) {
78
fullPath := qrcode.GetQrCodeFullPath()
79
fileName, path, err := a.Qr.Encode(fullPath)
80
if err != nil {
81
return "", "", err
82
}
83
84
if !a.CheckMergedImage(path) {
85
mergedF, err := a.OpenMergedImage(path)
86
if err != nil {
87
return "", "", err
88
}
89
defer mergedF.Close()
90
91
bgF, err := file.MustOpen(a.Name, path)
92
if err != nil {
93
return "", "", err
94
}
95
defer bgF.Close()
96
97
qrF, err := file.MustOpen(fileName, path)
98
if err != nil {
99
return "", "", err
100
}
101
defer qrF.Close()
102
103
bgImage, err := jpeg.Decode(bgF)
104
if err != nil {
105
return "", "", err
106
}
107
qrImage, err := jpeg.Decode(qrF)
108
if err != nil {
109
return "", "", err
110
}
111
112
jpg := image.NewRGBA(image.Rect(a.Rect.X0, a.Rect.Y0, a.Rect.X1, a.Rect.Y1))
113
114
draw.Draw(jpg, jpg.Bounds(), bgImage, bgImage.Bounds().Min, draw.Over)
115
draw.Draw(jpg, jpg.Bounds(), qrImage, qrImage.Bounds().Min.Sub(image.Pt(a.Pt.X, a.Pt.Y)), draw.Over)
116
117
jpeg.Encode(mergedF, jpg, nil)
118
}
119
120
return fileName, path, nil
121
}
Copied!
这里重点留意 func (a *ArticlePosterBg) Generate() 方法,做了如下事情:
    获取二维码存储路径
    生成二维码图像
    检查合并后图像(指的是存放合并后的海报)是否存在
    若不存在,则生成待合并的图像 mergedF
    打开事先存放的背景图 bgF
    打开生成的二维码图像 qrF
    解码 bgF 和 qrF 返回 image.Image
    创建一个新的 RGBA 图像
    在 RGBA 图像上绘制 背景图(bgF)
    在已绘制背景图的 RGBA 图像上,在指定 Point 上绘制二维码图像(qrF)
    将绘制好的 RGBA 图像以 JPEG 4:2:0 基线格式写入合并后的图像文件(mergedF)

错误码

路由方法

打开 routers/api/v1/article.go 文件,修改 GenerateArticlePoster 方法,编写最终的业务逻辑(含生成二维码及合并海报),如下:
1
const (
2
QRCODE_URL = "https://github.com/EDDYCJY/blog#gin%E7%B3%BB%E5%88%97%E7%9B%AE%E5%BD%95"
3
)
4
5
func GenerateArticlePoster(c *gin.Context) {
6
appG := app.Gin{c}
7
article := &article_service.Article{}
8
qr := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto) // 目前写死 gin 系列路径,可自行增加业务逻辑
9
posterName := article_service.GetPosterFlag() + "-" + qrcode.GetQrCodeFileName(qr.URL) + qr.GetQrCodeExt()
10
articlePoster := article_service.NewArticlePoster(posterName, article, qr)
11
articlePosterBgService := article_service.NewArticlePosterBg(
12
"bg.jpg",
13
articlePoster,
14
&article_service.Rect{
15
X0: 0,
16
Y0: 0,
17
X1: 550,
18
Y1: 700,
19
},
20
&article_service.Pt{
21
X: 125,
22
Y: 298,
23
},
24
)
25
26
_, filePath, err := articlePosterBgService.Generate()
27
if err != nil {
28
appG.Response(http.StatusOK, e.ERROR_GEN_ARTICLE_POSTER_FAIL, nil)
29
return
30
}
31
32
appG.Response(http.StatusOK, e.SUCCESS, map[string]string{
33
"poster_url": qrcode.GetQrCodeFullUrl(posterName),
34
"poster_save_url": filePath + posterName,
35
})
36
}
Copied!
这块涉及到大量知识,强烈建议阅读下,如下:
其所涉及、关联的库都建议研究一下

StaticFS

在 routers/router.go 文件,增加如下代码:
1
r.StaticFS("/qrcode", http.Dir(qrcode.GetQrCodeFullPath()))
Copied!

验证

image
访问完整的 URL 路径,返回合成后的海报并扫除二维码成功则正确 🤓
image

总结

在本章节实现了两个很常见的业务功能,分别是生成二维码和合并海报。希望你能够仔细阅读我给出的链接,这块的知识量不少,想要用好图像处理的功能,必须理解对应的思路,举一反三
最后希望对你有所帮助 👌

参考

本系列示例代码

关于

修改记录

    第一版:2018年02月16日发布文章
    第二版:2019年10月02日修改文章

如果有任何疑问或错误,欢迎在 issues 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

我的公众号

image
Last modified 2yr ago