【Go语言绘图】图片添加文字(一)
前一篇讲解了利用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()
的使用,并利用它们来实现了文字居中的效果。在下一篇中,将对通过另外几个方法的讲解来了解文字绘制的更多技巧。
如果本篇内容对你有帮助,别忘了点赞关注加收藏~
【Go语言绘图】图片添加文字(一)的更多相关文章
- R语言 如何为图片添加文字说明(转载)
转载:(中文翻译者)[http://blog.csdn.net/chen790646223/article/details/49766659] (原文链接)[http://datascienceplu ...
- ios图片添加文字或者水印
在项目中,我们会对图片做一些处理,但是我们要记住,一般在客户端做图片处理的数量不宜太多,因为受设备性能的限制,如果批量的处理图片,将会带来交互体验性上的一些问题.首先让我们来看看在图片上添加文字的方法 ...
- php图片添加文字水印方法汇总
方法一: <?php header("content-type:text/html;charset=utf-8"); //指定图片路径 $src = "img/a. ...
- php给图片添加文字水印方法汇总
在php中要给图片加水印我们需要给php安装GD库了,这里我们不介绍GD库安装,只介绍怎么利用php给图片添加文字水印的4种方法的汇总.有需要的小伙伴可以参考下. 1: 面向过程的编写方法 1 2 3 ...
- php 图片添加文字水印 以及 图片合成(微信快码传播)
1.图片添加文字水印: $bigImgPath = 'backgroud.png'; $img = imagecreatefromstring(file_get_contents($bigImgPat ...
- 利用php给图片添加文字水印--面向对象与面向过程俩种方法的实现
1: 面向过程的编写方法 //指定图片路径 $src = '001.png'; //获取图片信息 $info = getimagesize($src); //获取图片扩展名 $type = image ...
- 一种基于重载的高效c#上图片添加文字图形图片的方法
在做图片监控显示的时候,需要在图片上添加文字,如果用graphics类绘制图片上的字体,实现图像上添加自定义标记,这种方法经验证是可行的,并且在visual c#2005 编程技巧大全上有提到,但是, ...
- Swift - 给图片添加文字水印(图片上写文字,并可设置位置和样式)
想要给图片添加文字水印或者注释,我们需要实现在UIImage上写字的功能. 1,效果图如下: (在图片左上角和右下角都添加了文字.) 2,为方便使用,我们通过扩展UIImage类来实现添加水印功能 ( ...
- C# 使用 GDI+ 给图片添加文字,并使文字自适应矩形区域
需求 需求是要做一个编辑文字的页面.用户在网页端写文字,文字区域是个矩形框,用户可以通过下方的拖动条调节文字大小. 如下图: 提交数据的时候前端传文字区域的左上角和右下角定位给后台.因为前端的字体大小 ...
- C#图片添加文字水印
/// <summary> /// 给图片添加文字水印 /// </summary> /// <param name="img">图片</ ...
随机推荐
- 商业智能(BI)可视化大屏的设计及使用原则
信息时代,数据是一种可贵的资源,我们可能经常听到的一句话就是:用数据说话.但是,在没有进行系统化整理之前,数据不过只是一串串冰冷的数字,我们很难从大量的数据中获取到有价值的信息.只有通过合适的可视化工 ...
- Web 常见漏洞
检测到目标URL存在http host头攻击漏洞 描述:为了方便的获得网站域名,开发人员一般依赖于HTTP Host header.例如,在php里用_SERVER["HTTP_HOST&q ...
- web端项目如何测试
1.是否支持各种网络 2.网络如果演示能否正常加载 3.加载时断网还能加载出来么 4.浏览时断网页面是否保持 5.是否兼容各种不同的浏览器 6.不同的浏览器加载出的页面是否一致 7.页面效果如何 8. ...
- centos7中安装redis出现的问题
重现步骤: 1.解压redis包后 执行make命令.出现一堆东西,其中有gcc:命令未找到. 解决:安装 yum install gcc-c++(需要有网) 2.安装完gcc命令后,再make.出现 ...
- Python运算符的优先级是怎样的?
优先级数字越高表示优先级越高,有关运算符的详细介绍请参考<Python运算符大全>
- Hbase的基本原理(与HIVE的区别、数据结构模型、拓扑结构、水平分区原理、场景)
重点:HBase的基本数据模型.拓扑结构.部署配置方法,并介绍通过命令行和编程方式使用HBase的基本方法. HBase:一种列存储模式与键值对相结合的NoSQL软件,但更多的是使用列存储模式,底层的 ...
- js- for in 循环 只有一个目的,遍历 对象,通过对象属性的个数 控制循环圈数
for in 循环会返回 原型 以及原型链上面的属性,不会打印系统自带的属性 var obj ={ name:'suan', sex :'male', age:150, height:185, ...
- Java基础学习之HelloWorld(2)
前言 学习一门新的编程语言永远逃脱不了一场Hello World. 1.第一个程序 1.1.磁盘中新建一个文件 这里我们需要将文件后缀名显示出来,就是文件格式. 打开控制面板,取消隐藏已知文件类型的扩 ...
- Spring Cloud微服务学习笔记
Spring Cloud微服务学习笔记 SOA->Dubbo 微服务架构->Spring Cloud提供了一个一站式的微服务解决方案 第一部分 微服务架构 1 互联网应用架构发展 那些迫使 ...
- NET core 添加了新的nuget包,部署出现Could not load file or assembly
这个坑,今天整了一天,我添加了Microsoft.AspNetCore.Mvc.Versioning包,结果发布到服务器,我复制了dll过去出现了一直找不到加载不成功的问题 Startup.Confi ...