生成二维码、合并海报

项目地址:https://github.com/EDDYCJY/go-gin-example

知识点

  • 图片生成
  • 二维码生成

本文目标

在文章的详情页中,我们常常会需要去宣传它,而目前最常见的就是发海报了,今天我们将实现如下功能:

  • 生成二维码

  • 合并海报(背景图 + 二维码)

实现

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

生成二维码

安装

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

工具包

考虑生成二维码这一动作贴合工具包的定义,且有公用的可能性,新建 pkg/qrcode/qrcode.go 文件,写入内容:

  1. package qrcode
  2. import (
  3. "image/jpeg"
  4. "github.com/boombuler/barcode"
  5. "github.com/boombuler/barcode/qr"
  6. "github.com/EDDYCJY/go-gin-example/pkg/file"
  7. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  8. "github.com/EDDYCJY/go-gin-example/pkg/util"
  9. )
  10. type QrCode struct {
  11. URL string
  12. Width int
  13. Height int
  14. Ext string
  15. Level qr.ErrorCorrectionLevel
  16. Mode qr.Encoding
  17. }
  18. const (
  19. EXT_JPG = ".jpg"
  20. )
  21. func NewQrCode(url string, width, height int, level qr.ErrorCorrectionLevel, mode qr.Encoding) *QrCode {
  22. return &QrCode{
  23. URL: url,
  24. Width: width,
  25. Height: height,
  26. Level: level,
  27. Mode: mode,
  28. Ext: EXT_JPG,
  29. }
  30. }
  31. func GetQrCodePath() string {
  32. return setting.AppSetting.QrCodeSavePath
  33. }
  34. func GetQrCodeFullPath() string {
  35. return setting.AppSetting.RuntimeRootPath + setting.AppSetting.QrCodeSavePath
  36. }
  37. func GetQrCodeFullUrl(name string) string {
  38. return setting.AppSetting.PrefixUrl + "/" + GetQrCodePath() + name
  39. }
  40. func GetQrCodeFileName(value string) string {
  41. return util.EncodeMD5(value)
  42. }
  43. func (q *QrCode) GetQrCodeExt() string {
  44. return q.Ext
  45. }
  46. func (q *QrCode) CheckEncode(path string) bool {
  47. src := path + GetQrCodeFileName(q.URL) + q.GetQrCodeExt()
  48. if file.CheckNotExist(src) == true {
  49. return false
  50. }
  51. return true
  52. }
  53. func (q *QrCode) Encode(path string) (string, string, error) {
  54. name := GetQrCodeFileName(q.URL) + q.GetQrCodeExt()
  55. src := path + name
  56. if file.CheckNotExist(src) == true {
  57. code, err := qr.Encode(q.URL, q.Level, q.Mode)
  58. if err != nil {
  59. return "", "", err
  60. }
  61. code, err = barcode.Scale(code, q.Width, q.Height)
  62. if err != nil {
  63. return "", "", err
  64. }
  65. f, err := file.MustOpen(name, path)
  66. if err != nil {
  67. return "", "", err
  68. }
  69. defer f.Close()
  70. err = jpeg.Encode(f, code, nil)
  71. if err != nil {
  72. return "", "", err
  73. }
  74. }
  75. return name, path, nil
  76. }

这里主要聚焦 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. // Options are the encoding parameters.
  4. // Quality ranges from 1 to 100 inclusive, higher is better.
  5. type Options struct {
  6. Quality int
  7. }

路由方法

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. func GenerateArticlePoster(c *gin.Context) {
  5. appG := app.Gin{c}
  6. qrc := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto)
  7. path := qrcode.GetQrCodeFullPath()
  8. _, _, err := qrc.Encode(path)
  9. if err != nil {
  10. appG.Response(http.StatusOK, e.ERROR, nil)
  11. return
  12. }
  13. appG.Response(http.StatusOK, e.SUCCESS, nil)
  14. }

验证

通过 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. import (
  3. "image"
  4. "image/draw"
  5. "image/jpeg"
  6. "os"
  7. "github.com/EDDYCJY/go-gin-example/pkg/file"
  8. "github.com/EDDYCJY/go-gin-example/pkg/qrcode"
  9. )
  10. type ArticlePoster struct {
  11. PosterName string
  12. *Article
  13. Qr *qrcode.QrCode
  14. }
  15. func NewArticlePoster(posterName string, article *Article, qr *qrcode.QrCode) *ArticlePoster {
  16. return &ArticlePoster{
  17. PosterName: posterName,
  18. Article: article,
  19. Qr: qr,
  20. }
  21. }
  22. func GetPosterFlag() string {
  23. return "poster"
  24. }
  25. func (a *ArticlePoster) CheckMergedImage(path string) bool {
  26. if file.CheckNotExist(path+a.PosterName) == true {
  27. return false
  28. }
  29. return true
  30. }
  31. func (a *ArticlePoster) OpenMergedImage(path string) (*os.File, error) {
  32. f, err := file.MustOpen(a.PosterName, path)
  33. if err != nil {
  34. return nil, err
  35. }
  36. return f, nil
  37. }
  38. type ArticlePosterBg struct {
  39. Name string
  40. *ArticlePoster
  41. *Rect
  42. *Pt
  43. }
  44. type Rect struct {
  45. Name string
  46. X0 int
  47. Y0 int
  48. X1 int
  49. Y1 int
  50. }
  51. type Pt struct {
  52. X int
  53. Y int
  54. }
  55. func NewArticlePosterBg(name string, ap *ArticlePoster, rect *Rect, pt *Pt) *ArticlePosterBg {
  56. return &ArticlePosterBg{
  57. Name: name,
  58. ArticlePoster: ap,
  59. Rect: rect,
  60. Pt: pt,
  61. }
  62. }
  63. func (a *ArticlePosterBg) Generate() (string, string, error) {
  64. fullPath := qrcode.GetQrCodeFullPath()
  65. fileName, path, err := a.Qr.Encode(fullPath)
  66. if err != nil {
  67. return "", "", err
  68. }
  69. if !a.CheckMergedImage(path) {
  70. mergedF, err := a.OpenMergedImage(path)
  71. if err != nil {
  72. return "", "", err
  73. }
  74. defer mergedF.Close()
  75. bgF, err := file.MustOpen(a.Name, path)
  76. if err != nil {
  77. return "", "", err
  78. }
  79. defer bgF.Close()
  80. qrF, err := file.MustOpen(fileName, path)
  81. if err != nil {
  82. return "", "", err
  83. }
  84. defer qrF.Close()
  85. bgImage, err := jpeg.Decode(bgF)
  86. if err != nil {
  87. return "", "", err
  88. }
  89. qrImage, err := jpeg.Decode(qrF)
  90. if err != nil {
  91. return "", "", err
  92. }
  93. jpg := image.NewRGBA(image.Rect(a.Rect.X0, a.Rect.Y0, a.Rect.X1, a.Rect.Y1))
  94. draw.Draw(jpg, jpg.Bounds(), bgImage, bgImage.Bounds().Min, draw.Over)
  95. draw.Draw(jpg, jpg.Bounds(), qrImage, qrImage.Bounds().Min.Sub(image.Pt(a.Pt.X, a.Pt.Y)), draw.Over)
  96. jpeg.Encode(mergedF, jpg, nil)
  97. }
  98. return fileName, path, nil
  99. }

这里重点留意 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. func GenerateArticlePoster(c *gin.Context) {
  5. appG := app.Gin{c}
  6. article := &article_service.Article{}
  7. qr := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto) // 目前写死 gin 系列路径,可自行增加业务逻辑
  8. posterName := article_service.GetPosterFlag() + "-" + qrcode.GetQrCodeFileName(qr.URL) + qr.GetQrCodeExt()
  9. articlePoster := article_service.NewArticlePoster(posterName, article, qr)
  10. articlePosterBgService := article_service.NewArticlePosterBg(
  11. "bg.jpg",
  12. articlePoster,
  13. &article_service.Rect{
  14. X0: 0,
  15. Y0: 0,
  16. X1: 550,
  17. Y1: 700,
  18. },
  19. &article_service.Pt{
  20. X: 125,
  21. Y: 298,
  22. },
  23. )
  24. _, filePath, err := articlePosterBgService.Generate()
  25. if err != nil {
  26. appG.Response(http.StatusOK, e.ERROR_GEN_ARTICLE_POSTER_FAIL, nil)
  27. return
  28. }
  29. appG.Response(http.StatusOK, e.SUCCESS, map[string]string{
  30. "poster_url": qrcode.GetQrCodeFullUrl(posterName),
  31. "poster_save_url": filePath + posterName,
  32. })
  33. }

这块涉及到大量知识,强烈建议阅读下,如下:

其所涉及、关联的库都建议研究一下

StaticFS

在 routers/router.go 文件,增加如下代码:

  1. r.StaticFS("/qrcode", http.Dir(qrcode.GetQrCodeFullPath()))

验证

image

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

image

总结

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

最后希望对你有所帮助 👌

参考

本系列示例代码

关于

修改记录

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

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

我的公众号

image