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

這一篇將繼續介紹gg庫中繪製文字相關的方法,主要包括:DrawStringAnchored()DrawStringWrapped()MeasureMultilineString()WordWrap()下面來分別進行介紹。app

DrawStringAnchored

若是不細究,可能會以爲這個方法是 DrawString() 方法的一個封裝,但看看裏面的實現就能發現,實際狀況正好相反。函數

// DrawString draws the specified text at the specified point.
func (dc *Context) DrawString(s string, x, y float64) {
	dc.DrawStringAnchored(s, x, y, 0, 0)
}

// DrawStringAnchored draws the specified text at the specified anchor point.
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
// text. Use ax=0.5, ay=0.5 to center the text at the specified point.
func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
	w, h := dc.MeasureString(s)
	x -= ax * w
	y += ay * h
	if dc.mask == nil {
		dc.drawString(dc.im, s, x, y)
	} else {
		im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
		dc.drawString(im, s, x, y)
		draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over)
	}
}

DrawStringAnchored() 方法主要有5個參數,第一個參數是要繪製的字符串,後面四個參數共同決定了錨點的位置,具體計算邏輯是(x - w * ax, y - h * ay),因此,當axay設置爲0時就是左對齊,此時錨點位置處於文字框左下角;設置爲0.5時就是居中,此時錨點位置處於文字框正中央;設置爲1時就是右對齊,此時錨點位置處於文字控右上角。測試

咱們來看下效果:字體

func TestDrawStringAnchored(t *testing.T){
	const S = 1024
	dc := gg.NewContext(S, S)
	dc.SetRGB(1, 1, 1)
	dc.Clear()
	dc.SetRGB(0, 0, 0)
	if err := dc.LoadFontFace("gilmer-heavy.ttf", 96); err != nil {
		panic(err)
	}
	dc.DrawStringAnchored("Hello, world!", 0, dc.FontHeight(), 0, 0)
	dc.DrawStringAnchored("Hello, world!", S/2, S/2, 0.5, 0.5)
	dc.DrawStringAnchored("Hello, world!", S, S-dc.FontHeight(), 1, 1)
	dc.SavePNG("out.png")
}

這裏須要注意的就是錨點的位置,當左對齊時,錨點在左下角,因此設置的 (0, dc.FontHeight()) 表明的是文字框左下角的位置,同理,當居中對齊時,(S/2, S/2) 表明的是文字框中心點的位置,右對齊時,(S, S-dc.FontHeight()) 表明的是文字框右上頂點的位置。spa

DrawStringWrapped

這個方法能夠比較方便的繪製多行文字,還能自動折行,基本上至關於真正文字框的效果。3d

先看個例子簡單的熟悉一下:code

func TestDrawStringWrapped(t *testing.T){
	const S = 1024
	dc := gg.NewContext(S, S)
	dc.SetRGB(1, 1, 1)
	dc.Clear()
	dc.SetRGB(0, 0, 0)
	if err := dc.LoadFontFace("gilmer-heavy.ttf", 96); err != nil {
		panic(err)
	}
	dc.DrawStringWrapped("Hello world! Hello Frank! Hello Alice!", S/2, S/2, 0.5, 0.5, S, 1, gg.AlignCenter)
	dc.SavePNG("out.png")
}

繪製的效果以下:orm

能夠看到,不只自動換行,並且還保持了單詞的完整性,沒有將一個單詞從中間分割開來。blog

這個方法的參數有點多,一共有8個參數。圖片

第1個參數表明的是要繪製的字符串,好比這裏的Hello world! Hello Frank! Hello Alice!。第6個參數表明文本框的寬度。第7個參數表明行間距。

第2~5和第8個參數共同決定了錨點的位置。這裏的計算比以前稍微複雜一點,讓咱們來看看裏面的具體實現:

// DrawStringWrapped word-wraps the specified string to the given max width
// and then draws it at the specified anchor point using the given line
// spacing and text alignment.
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
	lines := dc.WordWrap(s, width)

	// sync h formula with MeasureMultilineString
	h := float64(len(lines)) * dc.fontHeight * lineSpacing
	h -= (lineSpacing - 1) * dc.fontHeight

	x -= ax * width
	y -= ay * h
	switch align {
	case AlignLeft:
		ax = 0
	case AlignCenter:
		ax = 0.5
		x += width / 2
	case AlignRight:
		ax = 1
		x += width
	}
	ay = 1
	for _, line := range lines {
		dc.DrawStringAnchored(line, x, y, ax, ay)
		y += dc.fontHeight * lineSpacing
	}
}

首先經過 WordWrap() 方法來獲得根據指定寬度處理事後的每一行須要展現的字符串信息。

lines := dc.WordWrap(s, width)

而後計算行高,這裏計算的時候是用行數乘以字體高度再乘以行間距,獲得結果後再減去一個行間距。因此這個 lineSpacing 的含義是行間距相對於字體高度的倍數,當 lineSpacing 設置爲1時,也就是行間距爲0,設置爲1.1時,表明行間距爲字體高度的0.1倍。

h := float64(len(lines)) * dc.fontHeight * lineSpacing
h -= (lineSpacing - 1) * dc.fontHeight

而後是有點繞的計算。

x -= ax * width
y -= ay * h
switch align {
case AlignLeft:
    ax = 0
case AlignCenter:
    ax = 0.5
    x += width / 2
case AlignRight:
    ax = 1
    x += width
}
ay = 1
for _, line := range lines {
    dc.DrawStringAnchored(line, x, y, ax, ay)
    y += dc.fontHeight * lineSpacing
}

能夠看到,總體邏輯是先計算好首行文字的錨點位置,而後對處理過的每一個字符串調用 DrawStringAnchored() 方法進行最終文字繪製。咱們能夠從下往上看,在循環繪製以前,先設置了 ay = 1,也就是說錨點的偏移位置會在每一行的頂部,而後咱們來看這個ax

switch align {
case AlignLeft:
    ax = 0
case AlignCenter:
    ax = 0.5
    x += width / 2
case AlignRight:
    ax = 1
    x += width
}

根據傳入的最後一個參數的不一樣值,ax 會設置爲不一樣的值。當最後一個參數分別爲 AlignLeftAlignCenterAlignRight時,axay 的組合分別爲:(0,1)(0.5,1)(1,1),錨點相對於單行文字的位置分別爲左上頂點、上中位置、右上頂點。

而後咱們再來看這個 y 的值:

y -= ay * h

y 的初始位置爲傳入的 y 值減去 ay (y軸偏移) 乘以總體文本框高度,表明的含義是初始錨點(x,y)相對於文本框的位置,分別傳入00.51時分別表明錨點處於文本框的上邊線、正中線和下邊線上。在循環繪製文字時,y 的值也會不斷調整,表明單行文字的錨點位置也在不斷變化。

y += dc.fontHeight * lineSpacing

最後來看下 x 的值,初始值爲初始錨點相對於傳入的文本框寬度的相對位置,ax 分別爲 00.51 時,分別表明初始錨點位於總體文本框的左邊線、居中豎線和右邊線上。

x -= ax * width

根據傳入的最後一個參數的不一樣,又會對x進行一次調整,這樣調整以後,便能實現文字在文本框中左對齊、居中和右對齊的效果了。

switch align {
case AlignLeft:
    ax = 0
case AlignCenter:
    ax = 0.5
    x += width / 2
case AlignRight:
    ax = 1
    x += width
}

看起來確實挺好用,不用再操心換行的事情了。但別高興的太早,有一點須要注意。這個方法只會根據空格來分割字符串,若是字符串沒有空格,就會變成只有一行文字的效果。

dc.DrawStringWrapped("HelloWorld!HelloFrank!HelloAlice!", S/2, S/2, 0.5, 0.5, S, 1, gg.AlignCenter)

你可能會以爲,英文單詞之間都會有空格的嘛,應該不用擔憂,但若是是中文呢?

if err := dc.LoadFontFace("/Users/bytedance/Downloads/font/方正楷體簡體.ttf", 96); err != nil {
    panic(err)
}
dc.DrawStringWrapped("若是咱們把文字換成中文效果就沒那麼好了", S/2, S/2, 0.5, 0.5, S, 1, gg.AlignCenter)

另外,這個方法不會限制文本框總體高度,因此若是文本很長,即便可能正確換行,仍舊會超出圖片範圍。

dc.DrawStringWrapped("好比這是一段很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 很長很長很長很長很長 的文字", S/2, S/2, 0.5, 0.5, S, 1, gg.AlignCenter)

另外,它是按照空格進行詞元素分割的,因此不會從單詞的中間進行拆分,這既是優勢,也是缺點。由於若是有長單詞的話,可能會致使提早換行,讓某些行看起來比其它行短不少。因此要想精確控制,仍是得用笨辦法。

MeasureMultilineString

MeasureMultilineString() 方法能夠測量多行文本的總體高度和寬度,須要傳入用換行符分割好的文本行字符串和行間距,裏面的計算邏輯也很簡單。

func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
	lines := strings.Split(s, "\n")

	// sync h formula with DrawStringWrapped
	height = float64(len(lines)) * dc.fontHeight * lineSpacing
	height -= (lineSpacing - 1) * dc.fontHeight

	d := &font.Drawer{
		Face: dc.fontFace,
	}

	// max width from lines
	for _, line := range lines {
		adv := d.MeasureString(line)
		currentWidth := float64(adv >> 6) // from gg.Context.MeasureString
		if currentWidth > width {
			width = currentWidth
		}
	}

	return width, height
}

行高的計算跟上面DrawStringWrapped()方法是同樣的:

h := float64(len(lines)) * dc.fontHeight * lineSpacing
h -= (lineSpacing - 1) * dc.fontHeight

寬度則是取這些文本行中寬度最大的那個。

WordWrap

這個方法是用來處理文本的,負責對文本根據指定寬度進行分行,在 DrawStringWrapped() 方法中已經有所調用。它內部是調用wordWrap()函數來實現的。

// WordWrap wraps the specified string to the given max width and current
// font face.
func (dc *Context) WordWrap(s string, w float64) []string {
	return wordWrap(dc, s, w)
}

wordWrap() 函數作的事情即是先將文字按換行符分割,而後對每個子字符串按空格進行分割,再經過一個元素一個元素的拼接來判斷出適合當前行寬的最大字符串。

func wordWrap(m measureStringer, s string, width float64) []string {
	var result []string
	for _, line := range strings.Split(s, "\n") {
		fields := splitOnSpace(line)
		if len(fields)%2 == 1 {
			fields = append(fields, "")
		}
		x := ""
		for i := 0; i < len(fields); i += 2 {
			w, _ := m.MeasureString(x + fields[i])
			if w > width {
				if x == "" {
					result = append(result, fields[i])
					x = ""
					continue
				} else {
					result = append(result, x)
					x = ""
				}
			}
			x += fields[i] + fields[i+1]
		}
		if x != "" {
			result = append(result, x)
		}
	}
	for i, line := range result {
		result[i] = strings.TrimSpace(line)
	}
	return result
}

須要注意的點

otf 字體文件加載

前面的內容中,加載字體文件都使用的是 LoadFontFace() 方法進行的,但須要注意的是,這個方法只能加載 ttf 字體文件,也就是 true type font,沒法加載 otf 字體文件,也就是 open type font。 因此若是須要加載 otf 字體文件,則須要換一個姿式。

func getOpenTypeFontFace(fontFilePath string, fontSize, dpi float64)(*font.Face, error){
	fontData, fontFileReadErr := ioutil.ReadFile(fontFilePath)
	if fontFileReadErr != nil {
		return nil, fontFileReadErr
	}
	otfFont, parseErr := opentype.Parse(fontData)
	if parseErr != nil {
		return nil, parseErr
	}
	otfFace, newFaceErr := opentype.NewFace(otfFont, &opentype.FaceOptions{
		Size: fontSize,
		DPI:  dpi,
	})
	if newFaceErr != nil {
		return nil, newFaceErr
	}
	return &otfFace, nil
}

來測試一下:

func TestUseOtfFile(t *testing.T){
	filePath := "SourceHanSansCN-Bold-2.otf"
	fontFace, err := getOpenTypeFontFace(filePath, 100, 82)
	if err != nil {
		panic(err)
	}

	const S = 1024
	dc := gg.NewContext(S, S)
	dc.SetRGB(0, 1, 1)
	dc.Clear()
	dc.SetRGB(0, 0, 0)
	dc.SetFontFace(*fontFace)
	dc.DrawStringWrapped("好比這是一段 很長很長很長 很長很長很長 的文字", S/2, S/2, 0.5, 0.5, S, 1, gg.AlignCenter)
	dc.SavePNG("out.png")
}

行高的問題

還有一個須要注意的問題,以前在開發時也踩過坑。SetFontFaceLoadFontFace 計算 fontHeight 時姿式不同,因此致使設置一樣的字體大小時,最終的字體高度卻不一致。

func (dc *Context) SetFontFace(fontFace font.Face) {
	dc.fontFace = fontFace
	dc.fontHeight = float64(fontFace.Metrics().Height) / 64
}

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
}

能夠看到對於行高的計算邏輯有着較大區別,咱們能夠用一個例子來簡單驗證一下:

func TestUseOtfFile(t *testing.T){
	filePath := "/Users/bytedance/Downloads/font/方正楷體簡體.ttf"
	fontFace1, err := getOpenTypeFontFace(filePath, 100, 82)
	if err != nil {
		panic(err)
	}

	const S = 1024
	dc := gg.NewContext(S, S)
	dc.SetRGB(0, 1, 1)
	dc.Clear()
	dc.SetRGB(0, 0, 0)
	dc.SetFontFace(*fontFace1)
	dc.DrawStringWrapped("好比這是一段文字", S/2, S/2, 0.5, 0.5, S, 1, gg.AlignCenter)
	if err := dc.LoadFontFace("/Users/bytedance/Downloads/font/方正楷體簡體.ttf", 100); err != nil {
		panic(err)
	}
	dc.DrawStringWrapped("好比這是一段文字", S/2, S/2 + 100, 0.5, 0.5, S, 1, gg.AlignCenter)
	dc.SavePNG("out.png")
}

能夠看到,兩行文字大小明顯不同。

小結

至此,關於文字繪製的相關內容就說完了。這兩篇講解了gg庫中關於文字繪製相關的內容,相信對於文字繪製已經有了比較好的掌握。實踐出真知,仍是須要多改改多用用才知道是怎麼一回事。在以後的一篇裏,會根據前面的內容進行一個小小的實戰應用,讓咱們的知識真正應用起來~

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

相關文章
相關標籤/搜索