【Go語言繪圖】圖片添加文字(一)

前一篇講解了利用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() 的使用,並利用它們來實現了文字居中的效果。在下一篇中,將對經過另外幾個方法的講解來了解文字繪製的更多技巧。

若是本篇內容對你有幫助,別忘了點贊關注加收藏~

相關文章
相關標籤/搜索