#前言

不得不感慨,前端的发展迅速以至于我觉得在前端样式下一小段 css 的事情,服务端实现起来无比复杂,甚至无法实现。
所以其实图片编辑的操作一直以来都是由前端或者客户端完成的。但,任何客户端的用户行为其实都是不可信的,对于一个图像裁剪,如果接入审核流又会过于繁重。服务端处理在一定程度上能够解决这个问题,如果这个场景其实不会特别频繁的话。

网上看了很多关于 golang 图像处理的,太复杂,甚至有的库调用了 open-cv,如果我都调用 open-cv 了= =,感觉也没你 go 什么事情了,python 不香吗。

#简单版本

还是以炉石的卡牌做例子,最近帮营地社区做了一个卡组的整合服务,可以看到外服(玩家已同意授权的前提下)前 200 玩家的卡组信息。

数据上只有诸如AAEBAf0EEoUX0MECtPwC9KsDxbgD4MwD5dED9tYDne4DoIoE5bAEhLIE27kEqd4El+8Eo5AF/cQF0fgFC8ABywS4tgOF5AOu9wPK3gSQlgWqmAXgwwXQ+AWxngYAAQOEuwL9xAXl0QP9xAWmkwX9xAUAAA==这样的代码,人不是机器,读不懂,所以需要解码,解码可以参考 用 golang 解析 varint

但其实图片是最直观的,抱着学习的态度,熟悉一下 golang 的图片操作。

整个流程简单下来可以概括成这样:

  1. 新建一个指定大小的画布(一张卡固定大小)
  2. 上底色
  3. 拿到原始素材
  4. 放缩比例
  5. 把这张图贴到我们的画布上(现实中是先裁切,再贴图,代码层面两者可以同时完成)

#新建画布

1
2
3
4
5
6
7
8
9
10
11
import (
"image"
)

ts := image.NewRGBA(image.Rectangle{
Min: image.Point{},
Max: image.Point{
X: 345,
Y: 50,
},
})

指定整个画布的大小,通过指定左上角和右下角的点的方式。

#上底色

笑死,简单的不用上底色

#放缩

1
2
3
4
5
6
7
8
9
import (
"github.com/nfnt/resize"
)

rawImage, _, err := getCardCutRaw(info.CardId)
if err != nil {
return
}
r2 := resize.Resize(450, 0, rawImage, resize.Lanczos3)

resize是个我看网上用的还比较多的包,本质上是指定长宽的放缩,但是如果你其中一个值不写,会默认等比例放缩。

原始素材来源于一个开源的平台:https://hearthstonejson.com/,走文件下载和缓存的方案就好。

1
2
3
4
5
6
7
8
9
var b []byte
b, err = tools.DownloadFile(fmt.Sprintf("https://art.hearthstonejson.com/v1/tiles/%s.png", cardId), filename)
if err != nil {
return
}
res, _, err = image.Decode(bytes.NewReader(b))
if err != nil {
return
}

但是需要注意的是,图像操作基本上的结构/接口都是image.Image,需要解码。

#贴图

是整个流程中的核心操作:

1
2
3
4
5
import (
"image/draw"
)

draw.Draw(ts, ts.Rect, r2, image.Point{X: 90, Y: 50}, draw.Over)
1
2
3
func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op) {
DrawMask(dst, r, src, sp, nil, image.Point{}, op)
}

Draw方法的函数定义如上,本质上是这样几个信息:

  1. 往哪里画
  2. 在最终的画布上预留给贴图的空间大小,是一个矩形框,用左上角和右下角的点表示。
  3. 要贴的图
  4. 从要贴的图的哪个点开始,往画布上贴(拷贝像素点?)
  5. 操作方式
1
2
3
4
5
6
7
8
9
// Op is a Porter-Duff compositing operator.
type Op int

const (
// Over specifies ``(src in mask) over dst''.
Over Op = iota
// Src specifies ``src in mask''.
Src
)

官方的文档其实我没太懂,而且也很复杂。但是根据我实际的操作结果来看,Over更像是贴的操作,这个自己尝试即可。

#导出

最后转成文件:

1
2
3
4
5
6
7
8
out, err := os.Create(filename)
if err != nil {
return
}
err = png.Encode(out, ts)
if err != nil {
return
}

#复杂版本

#上底色

1
2
3
4
5
gray := color.RGBA{R: 41, G: 46, B: 60, A: 255}
draw.Draw(ts, image.Rectangle{
Min: image.Point{X: 0},
Max: image.Point{X: 295, Y: 44},
}, &image.Uniform{C: gray}, image.Point{}, draw.Over)

image.Uniform形成一个单一颜色的矩形,贴入画布

#文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (
"github.com/golang/freetype"
)

fontBytes, err := os.ReadFile(filepath.Join(RawPath, "BelweBold.ttf"))
if err != nil {
panic(err)
}
font2, err := freetype.ParseFont(fontBytes)
if err != nil {
panic(err)
}
c2 := freetype.NewContext()
c2.SetDPI(72)
c2.SetFont(font2)
c2.SetFontSize(32)
c2.SetClip(ts.Bounds())
c2.SetDst(ts)
c2.SetHinting(xfont.HintingFull)

c2.SetSrc(image.Black)
pt3 := freetype.Pt(18, 34) // 字出现的位置
_, err = c2.DrawString(strconv.FormatInt(info.Cost, 10), pt3)
if err != nil {
panic(err)
}

c2.SetSrc(image.White)
pt2 := freetype.Pt(16, 32) // 字出现的位置
_, err = c2.DrawString(strconv.FormatInt(info.Cost, 10), pt2)
if err != nil {
panic(err)
}

最终下来效果其实是这样的:

卡牌代码

令我意外的是服务端的渲染速度出奇的快,没有想象中的延时。

#参考链接

#最后

如有不正确的地方,欢迎批评指正。