# 3.15 生成二维码、合并海报

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

## 知识点

* 图片生成
* 二维码生成

## 本文目标

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

* 生成二维码
* 合并海报（背景图 + 二维码）

## 实现

首先，你需要在 App 配置项中增加二维码及其海报的存储路径，我们约定配置项名称为 `QrCodeSavePath`，值为 `qrcode/`，经过多节连载的你应该能够完成，若有不懂可参照 [go-gin-example](https://github.com/EDDYCJY/go-gin-example/blob/master/conf/app.ini#L14)。

## 生成二维码

### 安装

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

### 工具包

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

```
package qrcode

import (
    "image/jpeg"

    "github.com/boombuler/barcode"
    "github.com/boombuler/barcode/qr"

    "github.com/EDDYCJY/go-gin-example/pkg/file"
    "github.com/EDDYCJY/go-gin-example/pkg/setting"
    "github.com/EDDYCJY/go-gin-example/pkg/util"
)

type QrCode struct {
    URL    string
    Width  int
    Height int
    Ext    string
    Level  qr.ErrorCorrectionLevel
    Mode   qr.Encoding
}

const (
    EXT_JPG = ".jpg"
)

func NewQrCode(url string, width, height int, level qr.ErrorCorrectionLevel, mode qr.Encoding) *QrCode {
    return &QrCode{
        URL:    url,
        Width:  width,
        Height: height,
        Level:  level,
        Mode:   mode,
        Ext:    EXT_JPG,
    }
}

func GetQrCodePath() string {
    return setting.AppSetting.QrCodeSavePath
}

func GetQrCodeFullPath() string {
    return setting.AppSetting.RuntimeRootPath + setting.AppSetting.QrCodeSavePath
}

func GetQrCodeFullUrl(name string) string {
    return setting.AppSetting.PrefixUrl + "/" + GetQrCodePath() + name
}

func GetQrCodeFileName(value string) string {
    return util.EncodeMD5(value)
}

func (q *QrCode) GetQrCodeExt() string {
    return q.Ext
}

func (q *QrCode) CheckEncode(path string) bool {
    src := path + GetQrCodeFileName(q.URL) + q.GetQrCodeExt()
    if file.CheckNotExist(src) == true {
        return false
    }

    return true
}

func (q *QrCode) Encode(path string) (string, string, error) {
    name := GetQrCodeFileName(q.URL) + q.GetQrCodeExt()
    src := path + name
    if file.CheckNotExist(src) == true {
        code, err := qr.Encode(q.URL, q.Level, q.Mode)
        if err != nil {
            return "", "", err
        }

        code, err = barcode.Scale(code, q.Width, q.Height)
        if err != nil {
            return "", "", err
        }

        f, err := file.MustOpen(name, path)
        if err != nil {
            return "", "", err
        }
        defer f.Close()

        err = jpeg.Encode(f, code, nil)
        if err != nil {
            return "", "", err
        }
    }

    return name, path, nil
}
```

这里主要聚焦 `func (q *QrCode) Encode` 方法，做了如下事情：

* 获取二维码生成路径
* 创建二维码
* 缩放二维码到指定大小
* 新建存放二维码图片的文件
* 将图像（二维码）以 JPEG 4：2：0 基线格式写入文件

另外在 `jpeg.Encode(f, code, nil)` 中，第三个参数可设置其图像质量，默认值为 75

```
// DefaultQuality is the default quality encoding parameter.
const DefaultQuality = 75

// Options are the encoding parameters.
// Quality ranges from 1 to 100 inclusive, higher is better.
type Options struct {
    Quality int
}
```

### 路由方法

1、第一步

在 routers/api/v1/article.go 新增 GenerateArticlePoster 方法用于接口开发

2、第二步

在 routers/router.go 的 apiv1 中新增 `apiv1.POST("/articles/poster/generate", v1.GenerateArticlePoster)` 路由

3、第三步

修改 GenerateArticlePoster 方法，编写对应的生成逻辑，如下：

```
const (
    QRCODE_URL = "https://github.com/EDDYCJY/blog#gin%E7%B3%BB%E5%88%97%E7%9B%AE%E5%BD%95"
)

func GenerateArticlePoster(c *gin.Context) {
    appG := app.Gin{c}
    qrc := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto)
    path := qrcode.GetQrCodeFullPath()
    _, _, err := qrc.Encode(path)
    if err != nil {
        appG.Response(http.StatusOK, e.ERROR, nil)
        return
    }

    appG.Response(http.StatusOK, e.SUCCESS, nil)
}
```

### 验证

通过 POST 方法访问 `http://127.0.0.1:8000/api/v1/articles/poster/generate?token=$token`（注意 $token）

![image](https://i.imgur.com/a6Ju16L.jpg)

通过检查两个点确定功能是否正常，如下：

1、访问结果是否 200

2、本地目录是否成功生成二维码图片

![image](https://i.imgur.com/FdVf4z4.jpg)

## 合并海报

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

### 背景图

![image](https://i.imgur.com/Zm13Wgv.jpg)

将背景图另存为 runtime/qrcode/bg.jpg（实际应用，可存在 OSS 或其他地方）

### service 方法

打开 service/article\_service 目录，新建 article\_poster.go 文件，写入内容：

```
package article_service

import (
    "image"
    "image/draw"
    "image/jpeg"
    "os"

    "github.com/EDDYCJY/go-gin-example/pkg/file"
    "github.com/EDDYCJY/go-gin-example/pkg/qrcode"
)

type ArticlePoster struct {
    PosterName string
    *Article
    Qr *qrcode.QrCode
}

func NewArticlePoster(posterName string, article *Article, qr *qrcode.QrCode) *ArticlePoster {
    return &ArticlePoster{
        PosterName: posterName,
        Article:    article,
        Qr:         qr,
    }
}

func GetPosterFlag() string {
    return "poster"
}

func (a *ArticlePoster) CheckMergedImage(path string) bool {
    if file.CheckNotExist(path+a.PosterName) == true {
        return false
    }

    return true
}

func (a *ArticlePoster) OpenMergedImage(path string) (*os.File, error) {
    f, err := file.MustOpen(a.PosterName, path)
    if err != nil {
        return nil, err
    }

    return f, nil
}

type ArticlePosterBg struct {
    Name string
    *ArticlePoster
    *Rect
    *Pt
}

type Rect struct {
    Name string
    X0   int
    Y0   int
    X1   int
    Y1   int
}

type Pt struct {
    X int
    Y int
}

func NewArticlePosterBg(name string, ap *ArticlePoster, rect *Rect, pt *Pt) *ArticlePosterBg {
    return &ArticlePosterBg{
        Name:          name,
        ArticlePoster: ap,
        Rect:          rect,
        Pt:            pt,
    }
}

func (a *ArticlePosterBg) Generate() (string, string, error) {
    fullPath := qrcode.GetQrCodeFullPath()
    fileName, path, err := a.Qr.Encode(fullPath)
    if err != nil {
        return "", "", err
    }

    if !a.CheckMergedImage(path) {
        mergedF, err := a.OpenMergedImage(path)
        if err != nil {
            return "", "", err
        }
        defer mergedF.Close()

        bgF, err := file.MustOpen(a.Name, path)
        if err != nil {
            return "", "", err
        }
        defer bgF.Close()

        qrF, err := file.MustOpen(fileName, path)
        if err != nil {
            return "", "", err
        }
        defer qrF.Close()

        bgImage, err := jpeg.Decode(bgF)
        if err != nil {
            return "", "", err
        }
        qrImage, err := jpeg.Decode(qrF)
        if err != nil {
            return "", "", err
        }

        jpg := image.NewRGBA(image.Rect(a.Rect.X0, a.Rect.Y0, a.Rect.X1, a.Rect.Y1))

        draw.Draw(jpg, jpg.Bounds(), bgImage, bgImage.Bounds().Min, draw.Over)
        draw.Draw(jpg, jpg.Bounds(), qrImage, qrImage.Bounds().Min.Sub(image.Pt(a.Pt.X, a.Pt.Y)), draw.Over)

        jpeg.Encode(mergedF, jpg, nil)
    }

    return fileName, path, nil
}
```

这里重点留意 `func (a *ArticlePosterBg) Generate()` 方法，做了如下事情：

* 获取二维码存储路径
* 生成二维码图像
* 检查合并后图像（指的是存放合并后的海报）是否存在
* 若不存在，则生成待合并的图像 mergedF
* 打开事先存放的背景图 bgF
* 打开生成的二维码图像 qrF
* 解码 bgF 和 qrF 返回 image.Image
* 创建一个新的 RGBA 图像
* 在 RGBA 图像上绘制 背景图（bgF）
* 在已绘制背景图的 RGBA 图像上，在指定 Point 上绘制二维码图像（qrF）
* 将绘制好的 RGBA 图像以 JPEG 4：2：0 基线格式写入合并后的图像文件（mergedF）

### 错误码

新增 [错误码](https://github.com/EDDYCJY/go-gin-example/blob/master/pkg/e/code.go#L27)，[错误提示](https://github.com/EDDYCJY/go-gin-example/blob/master/pkg/e/msg.go#L25)

### 路由方法

打开 routers/api/v1/article.go 文件，修改 GenerateArticlePoster 方法，编写最终的业务逻辑（含生成二维码及合并海报），如下：

```
const (
    QRCODE_URL = "https://github.com/EDDYCJY/blog#gin%E7%B3%BB%E5%88%97%E7%9B%AE%E5%BD%95"
)

func GenerateArticlePoster(c *gin.Context) {
    appG := app.Gin{c}
    article := &article_service.Article{}
    qr := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto) // 目前写死 gin 系列路径，可自行增加业务逻辑
    posterName := article_service.GetPosterFlag() + "-" + qrcode.GetQrCodeFileName(qr.URL) + qr.GetQrCodeExt()
    articlePoster := article_service.NewArticlePoster(posterName, article, qr)
    articlePosterBgService := article_service.NewArticlePosterBg(
        "bg.jpg",
        articlePoster,
        &article_service.Rect{
            X0: 0,
            Y0: 0,
            X1: 550,
            Y1: 700,
        },
        &article_service.Pt{
            X: 125,
            Y: 298,
        },
    )

    _, filePath, err := articlePosterBgService.Generate()
    if err != nil {
        appG.Response(http.StatusOK, e.ERROR_GEN_ARTICLE_POSTER_FAIL, nil)
        return
    }

    appG.Response(http.StatusOK, e.SUCCESS, map[string]string{
        "poster_url":      qrcode.GetQrCodeFullUrl(posterName),
        "poster_save_url": filePath + posterName,
    })
}
```

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

* [image.Rect](https://golang.org/pkg/image/#Rect)
* [image.Pt](https://golang.org/pkg/image/#Pt)
* [image.NewRGBA](https://golang.org/pkg/image/#NewRGBA)
* [jpeg.Encode](https://golang.org/pkg/image/jpeg/#Encode)
* [jpeg.Decode](https://golang.org/pkg/image/jpeg/#Decode)
* [draw.Op](https://golang.org/pkg/image/draw/#Op)
* [draw.Draw](https://golang.org/pkg/image/draw/#Draw)
* [go-imagedraw-package](https://blog.golang.org/go-imagedraw-package)

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

### StaticFS

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

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

### 验证

![image](https://i.imgur.com/kR1sqR6.jpg)

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

![image](https://i.imgur.com/0Mhmr44.jpg)

## 总结

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

最后希望对你有所帮助 👌

## 参考

### 本系列示例代码

* [go-gin-example](https://github.com/EDDYCJY/go-gin-example)

## 关于

### 修改记录

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

## ？

如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。

### 我的公众号

![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)
