• Golang Gin实践 连载十五 生成二维码、合并海报
    • 前言
    • 实现
    • 生成二维码
      • 安装
      • 工具包
      • 路由方法
      • 验证
    • 合并海报
      • 背景图
      • service 方法
      • 错误码
      • 路由方法
      • StaticFS
      • 验证
    • 总结
    • 参考
      • 本系列示例代码

    Golang Gin实践 连载十五 生成二维码、合并海报

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

    如果对你有所帮助,欢迎点个 Star ?

    前言

    在本章节,将实现如下功能细项:

    1、生成二维码

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

    实现

    首先,你需要在 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. }

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

    • image.Rect
    • image.Pt
    • image.NewRGBA
    • jpeg.Encode
    • jpeg.Decode
    • draw.Op
    • draw.Draw
    • go-imagedraw-package

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

    StaticFS

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

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

    验证

    image

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

    image

    总结

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

    最后希望对你有所帮助 ?

    参考

    本系列示例代码

    • go-gin-example