前一篇讲解了利用gg包来进行图片旋转的操作,这一篇我们来看看怎么在图片上添加文字。

绘制纯色背景

首先,我们先绘制一个纯白色的背景,作为添加文字的背景板。

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")}

输出图片如下:

这样我就得到了一张纯青色的背景图。回顾一下上一篇里绘制背景图的步骤:

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)}

添加文字

背景板已经准备就绪,接下来,我们来添加一些文字。

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!”就出现在了图片中央。

这里我们添加了三个步骤,首先是设置了字体颜色为黑色。

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() 的使用,并利用它们来实现了文字居中的效果。在下一篇中,将对通过另外几个方法的讲解来了解文字绘制的更多技巧。

如果本篇内容对你有帮助,别忘了点赞关注加收藏~

android 保留edittext中的文字不被后面添加的文字覆盖_【Go语言绘图】图片添加文字(一)...相关推荐

  1. android相对布局底部对齐,Android,在edittext中输入时防止相对布局底部对齐的按钮向上移动...

    我创建了一个包含3个列表视图的相对布局.在顶部,我有一个editText字段.在中间有一个滚动视图填充所有剩余空间.在底部我放了一个按钮.问题是,当我在editText字段中输入时,键盘会显示,然后按 ...

  2. Android下EditText中的字体不统一问题

    好久没写,今天心情好略记下解决的某bug 在一个登录界面有帐号和密码两个EditText,但是却发现两个EditText的hint的英文字体不同,看着极不协调.但是两个EditText都没有特意设置过 ...

  3. android 键盘 能复制,android – 从EditText中禁用软键盘,但仍允许复制/粘贴?

    经过几个小时的研究,我终于找到了适用于所有API版本的解决方案.希望这可以节省某人的时间. 如果您正在开发API> = 11,解决方案很简单: 1)在EditText的xml文件中添加以下两个属 ...

  4. android字体不统一,Android下EditText中的字体不统一问题

    好久没写,今天心情好略记下解决的某bug 在一个登录界面有帐号和密码两个EditText,但是却发现两个EditText的hint的英文字体不同,看着极不协调.但是两个EditText都没有特意设置过 ...

  5. android 阿拉伯语下的光标,android – 在EditText中的游标提示不是从右边开始的阿拉伯语...

    我正在尝试使用提示文本制作EditText: 用英文"password"..光标正确设置在左侧. 但对于提示为"كلمهالمرور"的阿拉伯语,光标始终设置为 ...

  6. android edittext 表情,Android在一个edittext中添加更多表情符号?

    您可以根据需要向Spannable添加任意数量的ImageSpans.只需遵循您链接的代码所规定的概念即可.您可能也想使用SpannableStringBuilder. Drawable happyS ...

  7. html语言移动图片,HTML文字和图片的移动

    移动的一些属性及参数: MARQUEE (令文字.图片移动) align=center>表示居中 移动方向 :direction="一个参数", 参数有:left(左).ri ...

  8. android edittext 手机号码,Android中EditText中的电话号码格式

    在Android的EditText中输入电话号码时,如何设置(xxx)xxx-xxxxx格式的电话号码? 我想要(,),-字符自动添加到特定位置. 我写了代码,但仅在andorid 2.2版本中有效, ...

  9. android radiobutton 文字按钮的距离,android RadioButton 图片与文字间距问题

    在使用radiobutton  的按钮跟文字之间的间距在不同的手机上会出现间距不一致,今天学习到了如何解决这个问题: android:button=@null;//将默认的button图片清空 and ...

  10. Android开发笔记(九十八)往图片添加部件

    添加圆角 添加圆角的功能,要用到Canvas类的drawRoundRect方法,即把画布裁剪成指定的圆角矩形. 下面是给图片添加圆角的效果截图: 下面是给图片添加圆角的代码片段: public sta ...

最新文章

  1. 元学习—Meta Learning的兴起
  2. python pycurl
  3. ACM-ICPC 2019 山东省省赛 C Wandering Robot
  4. 学习React的一知半解
  5. 部署Symantec Antivirus 10.0网络防毒服务器之七
  6. windows2019安装mysql 5.7
  7. idea module取得是parent的文件路径_React(或使用TS)中样式混乱解决方案 *.module.less...
  8. ThinkpadT470接通电源开机显示电量0%充不进电且电源指示灯不亮的解决办法
  9. Effects on PV3D
  10. 计算机一级excel典型试题,最新excel计算机一级试题合集
  11. C/C++编写Windows窗口应用程序(Win32程序),非黑漆漆的控制台窗口
  12. 西门子1200plc两部六层电梯程序
  13. 青春岁月杂志青春岁月杂志社青春岁月编辑部2022年第11期目录
  14. 封装R0805跟R0603贴片电阻有什么区别?
  15. 集成学习:lightGBM(二)
  16. c++:利用socket基于TPC/IP实现通信 在线聊天
  17. 计算机基础之Linux
  18. 网页三剑客:HTML+CSS+JavaScript 之CSS概述
  19. 瑞星安全随身WiFi:为用户WiFi上网安全保驾护航
  20. 中国年均温等值线分布图的制作

热门文章

  1. 关于Mac下python和pycharm的异常点
  2. HUffman树学习笔记
  3. 二维点云数据椭圆拟合算法及C++实现
  4. 最新:2021年度泰晤士世界大学学科排名公布
  5. Web前端开发规范 之html命名规范
  6. 隐藏WIN10资源管理器中的3D对象文件夹
  7. Nashorn Multithreading and MT-safety
  8. cf500B New Year Permutation
  9. spring与jpa整合 简化persistence.xml配置文件 使用属性文件 数据源dbcp访问数据库...
  10. (CMA-ES源码)协方差自适应进化策略(Covariance Matrix Adaptation Evolution Strategy,CMA-ES)——最好的单目标进化算法?