最近的项目中需要用代码来手工绘图,例如设置背景图片、贴矩形、贴文字至指定位置等等,但是标准库中的image包又有些偏向底层了,于是想找一个go语言中可用性较高的2D渲染绘图封装库,寻找了一番,个人感觉最合适的便是fogleman/gg了:官方文档。
理解了gg包的一些基本概念之后,它还是很便于绘制2D图像的,预置了不少类似但不限于画圆、画方、画线、填充、描边、旋转、缩放、文字处理、剪切、蒙版、翻转的接口,那么,开始吧。
基本使用如下代码:
package main
import "github.com/fogleman/gg"
func main() {
dc := gg.NewContext(512, 512)
dc.SetRGB255(255,222,173)
dc.Clear()
dc.SavePNG("out.png")
}
执行后,你便得到了一张黄图(纯黄色的图
解析
第一行,使用gg.NewContext创建了一个大小为512x512的画布dc。
第二行,通过dc.SetRGB255将当前画笔颜色设置为RGB 255,222,173,即黄色。
第三行,Clear方法将会使用当前画笔颜色填充满整个画布。
第四行,将画布保存成PNG格式图片。
基本概念要想理解gg包的用法,首先要掌握一组基本概念,但此类内容网上又找不到任何参考资料,因此下述内容皆为我(RicheyJang)的个人理解,欢迎交流指正!
gg包的概念继承并改造于官方的freetype包,仅对于使用gg包来说,需要了解以下概念:
画布(Context):直译过来以及对于golang标准概念而言,它应该被成为上下文,但是为了便于理解,我称之为画布,是一张定义中、描绘中的图片。
路径(path):路径是一组子路径。
子路径(subpath):子路径是一个起点,加一组首尾相连的线性、二次或三次贝塞尔曲线(不一定闭合)。
当前路径(current path):当前正在定义(不一定已经真正画到画布上)的路径。
当前点(current point):画布上的一个点(像素点),标志了当前画笔所在的位置。也就是当前子路径的尾点。
基本底层操作首先是直接针对路径的操作:
dc := gg.NewContext(w, h) // 创建宽为w、长为h的新画布。
dc.Fill()
// Fill 将会使用当前画笔颜色填充满当前路径所闭合出的区域(当前路径中的各个子路径会被gg隐式闭合)。
dc.FillPreserve()
// FillPreserve 将会使用当前画笔颜色填充满当前路径所闭合出的区域(当前路径中的各个子路径会被gg隐式闭合)。
// 它与Fill方法的区别是,Fill将会一并删除当前路径,而FillPreserve不会。
dc.Stroke()
// Stroke 将会把当前路径使用当前画笔颜色描绘到画布上。
dc.StrokePreserve()
// StrokePreserve 它与Stroke方法的区别类比上述Fill与FillPreserve。
上述几个方法为非常常用的绘图方法,下面便是一些更偏底层的用于绘制路径本身的方法了。
dc.NewSubPath()
// NewSubPath 会在当前路径中新建一条子路径,会使得"当前点"不存在。
dc.ClearPath()
// ClearPath 将会清除当前路径。
dc.ClosePath()
// ClosePath 会添加一条起点为当前点、终点为当前子路径起点线性线段。用于显式地闭合当前子路径。
dc.MoveTo(x, y)
// MoveTo 会新建一条起点为(x,y)的子路径,并将当前点设为(x,y)。
dc.LineTo(x, y)
// LineTo 会向当前子路径添加一条起点为当前点、终点为(x,y)的线性线段,并将当前点设为(x,y)。若当前点不存在,它等同于MoveTo(x,y)。
不过,不会真有人直接只用上述路径操作来画图吧,那也太麻烦了(;´д`)ゞ。
gg包封装了一组以Draw打头的方法,用于便携地画出圆形、矩形、图像、文字等等路径或者贴图,不过此类内容,干说不如实操,请参阅后述具体操作章节。
接下来是一组针对画笔的操作:
dc.SetLineWidth(lineWidth)
// SetLineWidth 设置画笔宽度为lineWidth,会在Stroke时体现出来。
dc.SetFontFace(fontFace)
// SetFontFace 设置画笔字体为fontFace,fontFace为font.Face类型。
// 也可以使用LoadFontFace(path string, points float64)方法从本地路径加载字体文件。
dc.SetColor(c)
// SetColor 设置当前画笔颜色为c,c为color.Color类型。
关于设置画笔颜色,还有一系列封装方法,例如:(省略了dc.)
SetHexColor(x string) // 使用Hex表达式,例如"#fafafa"
SetRGB(r, g, b float64) // 使用RGB值,0<=r,g,b<=1
SetRGB255(r, g, b int) // 更常用地,使用整数RGB值,0<=r,g,b<=255
SetRGBA(r, g, b, a float64)// 使用RGB值,a为不透明度,0<=r,g,b,a<=1
具体操作:
看了半天,怪累的,下面让我们进入实操环境叭!
示例1:画圆
package main
import "github.com/fogleman/gg"
func main() {
dc := gg.NewContext(512, 512)
dc.DrawCircle(250,250,200) // 添加一条以(250,250)为圆心、200为半径的圆形子路径
dc.SetRGB255(255,222,173) // 设置画笔颜色为黄色
dc.Fill() // 使用当前颜色(黄色)填充满当前路径(圆)所闭合出的区域
dc.SavePNG("out.png")
}
另外,gg包中的坐标计算:(0,0)点位于画布的最左上角,横向为x轴,纵向为y轴。
效果:
示例2:画带边框的矩形
package main
import "github.com/fogleman/gg"
func main() {
dc := gg.NewContext(512, 512)
dc.DrawRectangle(50, 50, 300, 233) // 添加一条以(50,50)为左上点、宽300、长233的矩形子路径
dc.SetLineWidth(5) // 设置画笔宽度为5像素
dc.SetRGBA255(245,34,45, 255 / 2) // 设置画笔颜色为红色,且半透明
dc.StrokePreserve() // 使用当前颜色(红)描出当前路径(矩形),但不删除当前路径
dc.SetHexColor("#b7eb8f") // 设置画笔颜色为绿色
dc.Fill() // 使用当前颜色(绿)填充满当前路径(矩形)所闭合出的区域
dc.SavePNG("out.png")
}
效果:
示例3:贴图片文件
我们可以直接使用DrawImage(im image.Image, x, y int)方法来将一张已有图片贴到画布上,与上述内容不同的是,该方法不会描绘出一条新的子路径,而是将图片直接贴到画布上,这一点请注意。
package main
import (
"fmt"
"github.com/fogleman/gg"
)
func main() {
img,err := gg.LoadImage("./paimeng.jpeg") // gg包预置的将本地图片载入成image.Image结构的函数
if err != nil {
fmt.Println(err)
return
}
dc := gg.NewContext(800, 800)
dc.SetHexColor("#b7eb8f") // 设置画笔颜色为绿色
dc.Clear() // 使用当前颜色(绿)填满画布,即设置背景色
dc.DrawImage(img, 50,100) // 以(50,100)为左上角,贴入img图片
dc.SavePNG("out.png")
}
效果:
默认的DrawImage(im image.Image, x, y int)方法,其定位方式为:图像的左上角位于画布上的(x,y)点。但有的时候,我们想让图像居中位置或右下角位于(x,y),此类情况无需我们自己进行计算,gg包已经考虑好了,使用DrawImageAnchored方法即可:
func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64)
DrawImageAnchored方法会令图像的左上角位于 ( x − w ∗ a x , y − h ∗ a y ) (x - w * ax, y - h * ay) (x−w∗ax,y−h∗ay),其中,w、h分别为所贴入图像的宽和长。也就是说,当指定ax=ay=0.5时,图像的居中位置会位于(x,y),当ax=ay=1时,图像的右下角会位于(x,y),而对于DrawImage方法而言,其ax=ay=0。
示例4:贴文字
同贴图一样,贴文字也是会直接将文字贴入画布,而非绘制路径。
package main
import (
"github.com/fogleman/gg"
)
func main() {
S := float64(600)
str :=
`一段挺长长长长长长长长长长长的文字
甚至还有换行`
dc := gg.NewContext(int(S), int(S))
dc.SetHexColor("#b7eb8f") // 设置画笔颜色为绿色
dc.Clear() // 使用当前颜色(绿)填满画布,即设置背景色
if err := dc.LoadFontFace("./zh-cn.ttf", 30); err != nil { // 从本地加载字体文件
panic(err)
}
dc.SetRGB(0,0,0) // 设置画笔颜色为黑色
sWidth, sHeight := dc.MeasureString(str) // 测算字符串将在画布中占用的宽与长
dc.DrawString(str, (S-sWidth)/2, (S+sHeight)/2) // 直接将文字贴入画布中
// 文本框格式化文字,参见下方详解
dc.DrawStringWrapped(str, S/2, S/2 + 100, 0.5, 0.5, S - 10, 1.3, gg.AlignCenter)
dc.SavePNG("out.png")
}
效果:
代码中出现的DrawStringWrapped方法,通过类似文本框的方式,会自动地对要贴入的文字进行换行、偏移等操作。定义如下:
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align)
其中,s为待贴入字符串;(x,y)为定位点,ax,ay参考上述DrawImageAnchored;lineSpacing为行距,例如,lineSpacing=1.3即为1.3倍行距,当其为0时,多行文字将重叠;align为对齐方式,包含AlignLeft、AlignCenter、AlignRight。
另外,代码中出现的MeatureString方法,它并不会对画布产生任何影响,只会测算字符串被放入画布后所会占用的宽度、高度;对于多行字符串,可以使用MeasureMultilineString方法:
func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64)
示例5:半透明+缩放贴图
来个稍微复杂一点的。现在我需要读入一张未知尺寸的图片,作为一张即将生成的1024x1024大小图像的背景图,且需要将其设为半透明。也就是说,需要做的工作有:读入图片、缩放、设置不透明度、贴图。
上代码!
package main
import (
"fmt"
"image"
"image/color"
"github.com/fogleman/gg"
)
// AdjustOpacity 将输入图像m的透明度变为原来的倍数。若原来为完成全不透明,则percentage = 0.5将变为半透明
func AdjustOpacity(m image.Image, percentage float64) image.Image {
bounds := m.Bounds()
dx := bounds.Dx()
dy := bounds.Dy()
newRgba := image.NewRGBA64(bounds)
for i := 0; i < dx; i++ {
for j := 0; j < dy; j++ {
colorRgb := m.At(i, j)
r, g, b, a := colorRgb.RGBA()
opacity := uint16(float64(a) * percentage)
v := newRgba.ColorModel().Convert(color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: opacity})
_r, _g, _b, _a := v.RGBA()
newRgba.SetRGBA64(i, j, color.RGBA64{R: uint16(_r), G: uint16(_g), B: uint16(_b), A: uint16(_a)})
}
}
return newRgba
}
func main() {
bg, err := gg.LoadImage("./paimeng.jpeg")
if err != nil {
fmt.Println(err)
return
}
bg = AdjustOpacity(bg, 0.5) // 设置不透明度
S := 1024
dc := gg.NewContext(S, S)
sx := float64(S) / float64(bg.Bounds().Size().X) // 计算缩放倍率(宽)
sy := float64(S) / float64(bg.Bounds().Size().Y) // 计算缩放倍率(长)
// 设置背景
dc.Scale(sx, sy) // 使画笔按倍率缩放
dc.DrawImage(bg, 0, 0) // 贴图(会受上述缩放倍率影响)
dc.SavePNG("out.png")
}
效果:
如果后续此文有人看的话,我再更新一下gg包中的文字处理、剪切、蒙版、翻转、贝塞尔曲线等内容叭。