前一篇讲解了利用gg包来进行图片旋转的操做,这一篇咱们来看看怎么在图片上添加文字。git
首先,咱们先绘制一个纯白色的背景,做为添加文字的背景板。github
package main import "github.com/fogleman/gg" func main() { const S = 1024 dc := gg.NewContext(S, S) dc.SetRGB(0, 1, 1) dc.Clear() dc.SavePNG("out.png") }
输出图片以下:数组
这样我就获得了一张纯青色的背景图。回顾一下上一篇里绘制背景图的步骤:app
func TestRotateImage(t *testing.T) { width := 1000 height := 1000 dc := gg.NewContext(width, height) dc.DrawRectangle(0, 0, float64(width), float64(width)) dc.SetRGB255(255, 255, 0) dc.Fill() dc.SavePNG("test.png") }
咱们是经过先绘制跟画布一样大小的矩形,而后将它的颜色进行填充来实现纯色背景效果的,但实际上使用 Clear()
方法便能直接使用当前颜色对画布进行填充。函数
查看一下 Clear()
方法便能发现,里面是经过调用 draw.Draw()
函数来实现的,这也是go语言自带的 image
包里颇有用的一个函数,后面会有文章来作更详细的介绍。简单来讲,Clear()
方法是经过调用draw.Draw()
函数,经过将纯色图片覆盖到原画布的方式来实现纯色背景的效果的。字体
// Clear fills the entire image with the current color. func (dc *Context) Clear() { src := image.NewUniform(dc.color) draw.Draw(dc.im, dc.im.Bounds(), src, image.ZP, draw.Src) }
背景板已经准备就绪,接下来,咱们来添加一些文字。3d
package main import "github.com/fogleman/gg" func main() { const S = 1024 dc := gg.NewContext(S, S) dc.SetRGB(0, 1, 1) dc.Clear() dc.SetRGB(0, 0, 0) if err := dc.LoadFontFace("gilmer-heavy.ttf", 120); err != nil { panic(err) } dc.DrawString("Hello, world!", 0, S/2) dc.SavePNG("out.png") }
输出以下,一个硕大、黑色的“Hello, World!”就出如今了图片中央。code
这里咱们添加了三个步骤,首先是设置了字体颜色为黑色。orm
dc.SetRGB(0, 0, 0)
而后加载了字体文件,这里须要注意的是,经过 LoadFontFace()
方法加载的字体文件只支持 ttf
后缀的文件,也就是 true type font
,对象
if err := dc.LoadFontFace("gilmer-heavy.ttf", 120); err != nil { panic(err) }
里面的实现也比较简单:
func (dc *Context) LoadFontFace(path string, points float64) error { face, err := LoadFontFace(path, points) if err == nil { dc.fontFace = face dc.fontHeight = points * 72 / 96 } return err }
内部调用了 LoadFontFace()
函数,在这个函数内部进行了字体文件读取,并用 freetype
包里的Parse()
函数进行字体的加载,最后在调用 NewFace()
函数来建立一个 font.Face
对象,在外面的LoadFontFace()
方法里,将这个对象保存在 fontFace
字段中,而且根据传入的point
大小设置了一下字体高度。
至于为何是乘以72
而后除以96
,这个查了一下资料,简单的说,字体的大小单位磅(points
) 是1/72
逻辑英寸,屏幕的分辨率是96DPI
(96点每逻辑英寸),那么屏幕每一个点就是72/96
=0.75
磅。
func LoadFontFace(path string, points float64) (font.Face, error) { fontBytes, err := ioutil.ReadFile(path) if err != nil { return nil, err } f, err := truetype.Parse(fontBytes) if err != nil { return nil, err } face := truetype.NewFace(f, &truetype.Options{ Size: points, // Hinting: font.HintingFull, }) return face, nil }
若是想调整字体大小,也很简单,只须要调整LoadFontFace()
方法传入的值便可,让咱们来调大一点字体看看效果。
if err := dc.LoadFontFace("gilmer-heavy.ttf", 240); err != nil { panic(err) }
这样就大不少了。不知道聪明的你注意到了没有,在调用dc.DrawString("Hello, world!", 0, S/2)
时,咱们设置的坐标是 (0, S/2)
,也就是左侧边的正中心点,这个位置恰好是绘制出来的文字的左下角的坐标,这是须要注意的一点。
若是想要文字居中显示怎么办呢?好比咱们想要这个 Hello,World!
显示在图片的正中央,要怎么处理呢?一个笨办法固然是经过调整字体位置来实现这个效果,让咱们先来试试:
if err := dc.LoadFontFace("gilmer-heavy.ttf", 120); err != nil { panic(err) } dc.DrawString("Hello, world!", 130, S/2)
经过屡次调整,字体大小设置为120
时,x
的位置设置为130
,基本上能够看起来是居中的。但这样的话每次换文字都得反复调整位置,显然不科学。
别慌,有一个方法能够获得文字的宽度,MeasureString()
能够获得在当前字体下指定字符串的宽度和高度,这个高度其实就是前面经过 points * 72 / 96
计算获得的,而后咱们再将左下角的位置设置为((S-sWidth)/2, (S+sHeight)/2)
便可实现文字居中的效果,注意y轴坐标是(S+sHeight)/2
,由于文字的左上顶点位置y轴坐标应该是(S-sHeight)/2
,左下顶点坐标只须要再加上字体高度便可得出。
s := "Hello, world!" sWidth, sHeight := dc.MeasureString(s) dc.DrawString(s, (S-sWidth)/2, (S+sHeight)/2)
这样看来,居中显示也不过如此嘛。但别高兴的太早,有没有想过,若是文字过长该怎么处理?好比咱们来调整一下文字内容,再看下生成的效果。
s := "Hello,world! Hello,ByteDancer!"
文字已经超出边界了,显然不是理想的效果,这个时候有两种处理方法,一种是添加省略号,一种是换行。
先来讲一下添加省略号的处理方案,听起来好像挺简单,但实际上处理起来也挺麻烦的。
首先须要肯定一个文字展现的最大宽度,由于若是满打满算整行都塞满文字显然很差看。其次是要逐个字符进行宽度计算,并判断是否会超过最大宽度,最后截取并保留恰好小于最大宽度时的字符串(须要考虑省略号的宽度)。
咱们来逐个处理。首先拍脑壳定一个文字最大宽度为图片宽度的0.75
倍。
maxTextWidth := S * 0.75
而后来逐个字符计算宽度,直到恰好大于最大宽度为止。
func TruncateText(dc *gg.Context, originalText string, maxTextWidth float64) string { tmpStr := "" for i := 0; i < len(originalText); i++ { tmpStr = tmpStr + string(originalText[i]) w, _ := dc.MeasureString(tmpStr) if w > maxTextWidth { return tmpStr[0 : i-1] } } return tmpStr }
而后咱们调整一下调用的地方。
func main() { const S = 1024 dc := gg.NewContext(S, S) dc.SetRGB(0, 1, 1) dc.Clear() dc.SetRGB(0, 0, 0) if err := dc.LoadFontFace("gilmer-heavy.ttf", 120); err != nil { panic(err) } s := "Hello,world! Hello,ByteDancer!" ellipsisWidth, _ := dc.MeasureString("...") maxTextWidth := S * 0.75 s = TruncateText(dc, s, maxTextWidth - ellipsisWidth) + "..." fmt.Println(s) sWidth, sHeight := dc.MeasureString(s) dc.DrawString(s, (S-sWidth)/2, (S+sHeight)/2) dc.SavePNG("out.png") }
这里咱们先计算了省略号的宽度,而后用最大字符串宽度减去省略号宽度做为最大宽度传入,获得最终要展现的字符串。生成的效果以下:
看起来好像没什么毛病,但若是咱们把文字换成中文,状况可能就不同了。咱们换一个中文字体,而后把字符串设置成中文。
if err := dc.LoadFontFace("方正楷体简体.ttf", 120); err != nil { panic(err) } s := "若是咱们把文字换成中文"
就变成了这个样子。
发现图片上只剩下了省略号,缘由是中文字符串分割不正确致使出现了乱码,而这个乱码在字体里找不到对应的文字,因此没法展现。这时,须要先将字符串先转化为rune
数组,或者经过直接对字符串使用 for range
遍历,能够避免在中文的状况出现乱码的状况。
func TruncateText(dc *gg.Context, originalText string, maxTextWidth float64) string { tmpStr := "" result := make([]rune, 0) for _, r := range originalText { tmpStr = tmpStr + string(r) w, _ := dc.MeasureString(tmpStr) if w > maxTextWidth { if len(tmpStr) <= 1 { return "" } else { break } } else { result = append(result, r) } } return string(result) }
这样文字就能按照咱们的预期进行展现了。
接下来,咱们来看看怎么处理多行文本,即当一行文字展现不下时,把文字切割成多行进行展现。若是咱们仍旧使用以前的方法来处理的话,就须要先计算好每行展现的字以及行数,而后再进行展现。
package main import ( "github.com/fogleman/gg" "strings" ) func main() { const S = 1024 dc := gg.NewContext(S, S) dc.SetRGB(0, 1, 1) dc.Clear() dc.SetRGB(0, 0, 0) if err := dc.LoadFontFace("/Users/bytedance/Downloads/方正楷体简体.ttf", 120); err != nil { panic(err) } s := "这是个人一个秘密,再简单不过的秘密:一我的只有用心去看,才能看到真实。事情的真相只用眼睛是看不见的。 --《小王子》" ellipsisWidth, _ := dc.MeasureString("...") maxTextWidth := S * 0.9 lineSpace := 25.0 maxLine := int(S / (dc.FontHeight() + lineSpace)) line := 0 lineTexts := make([]string, 0) for len(s) > 0 { line++ if line > maxLine { break } if line == maxLine { sw, _ := dc.MeasureString(s) if sw > maxTextWidth { maxTextWidth -= ellipsisWidth } } lineText := TruncateText(dc, s, maxTextWidth) if line == maxLine && len(lineText) < len(s) { lineText += "..." } lineTexts = append(lineTexts, lineText) if len(lineText) >= len(s) { break } s = s[len(lineText):] } lineY := (S - dc.FontHeight()*float64(len(lineTexts)) - lineSpace*float64(len(lineTexts)-1)) / 2 lineY += dc.FontHeight() for _, text := range lineTexts { sWidth, _ := dc.MeasureString(text) lineX := (S - sWidth) / 2 dc.DrawString(text, lineX, lineY) lineY += dc.FontHeight() + lineSpace } dc.SavePNG("out.png") } func TruncateText(dc *gg.Context, originalText string, maxTextWidth float64) string { tmpStr := "" result := make([]rune, 0) for _, r := range originalText { tmpStr = tmpStr + string(r) w, _ := dc.MeasureString(tmpStr) if w > maxTextWidth { if len(tmpStr) <= 1 { return "" } else { break } } else { result = append(result, r) } } return string(result) }
这段逻辑其实也很简单,首先根据行高和行间距计算出当前图片最多能展现多少行字,而后遍历须要展现的字符串进行逐行截取,截取出一行行的文字来。
遍历时有一个小细节,那就是判断是否已经到达最后一行,若是到达最后一行,则要考虑是否添加省略号了。
//若是已是最后一行,则须要判断剩余字符串是否仍旧超过最大宽度 if line == maxLine { sw, _ := dc.MeasureString(s) // 若是超过则须要在末尾添加省略号,截取的最大宽度须要减去省略号的宽度 if sw > maxTextWidth { maxTextWidth -= ellipsisWidth } } lineText := TruncateText(dc, s, maxTextWidth) // 若是是最后一行而且文字仍旧是被截取过,那么在末尾添加省略号 if line == maxLine && len(lineText) < len(s) { lineText += "..." }
在绘制文本时,先考虑整个文本框的左上顶点位置,由于须要居中展现,每一行的宽度是变化的,X轴坐标是不肯定的,可是Y轴坐标是能够先计算出来的,由于每一行的高度和行间距咱们都已经知道了。整个文本框的高度就是dc.FontHeight()*float64(len(lineTexts)) - lineSpace*float64(len(lineTexts)-1))
,用图片高度减去文本框高度再除以2,就能获得左上顶点高度了。
lineY := (S - dc.FontHeight()*float64(len(lineTexts)) - lineSpace*float64(len(lineTexts)-1)) / 2
而后开始逐行绘制文字,计算每一行的左下顶点X轴和Y轴坐标便可。
lineY += dc.FontHeight() for _, text := range lineTexts { sWidth, _ := dc.MeasureString(text) lineX := (S - sWidth) / 2 dc.DrawString(text, lineX, lineY) lineY += dc.FontHeight() + lineSpace }
最后的效果以下图:
这样虽然实现了效果,可是显然有些太过复杂,咱们还能再简化一下这个过程。
在gg库中,还有两个方法能够绘制文字,DrawStringAnchored()
和 DrawStringWrapped()
。前者能够在指定一个点为偏移起点。后者则相似于一个文本框的效果,能够指定文本框中心点和文本框宽度,这些将在下一篇中进行介绍。
这里的处理没有考虑原文本中有换行符的状况,因此其实还不够完善,在处理时能够先对文本进行换行符分割,而后再依次进行上述处理。
这一篇中,主要讲解了如何在纯色背景图上进行文字的绘制,说明了 DrawString()
方法和 MeasureString()
的使用,并利用它们来实现了文字居中的效果。在下一篇中,将对经过另外几个方法的讲解来了解文字绘制的更多技巧。
若是本篇内容对你有帮助,别忘了点赞关注加收藏~