# 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)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://eddycjy.gitbook.io/golang/di-3-ke-gin/image.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
