概述

最近的项目中需要用代码来手工绘图,例如设置背景图片、贴矩形、贴文字至指定位置等等,但是标准库中的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包中的文字处理、剪切、蒙版、翻转、贝塞尔曲线等内容叭。