Go 图片生成处理

Go 图片生成处理

在一次需求中使用Go做图片处理,简单来说,就是生成一张图片带有水印+文字。使用的, 特定写下此文,介绍一些在Go中处理图片的方式和一些第三库的使用。
对于图片的处理,后端通常来说有两种方式,前端处理的方式我就不列了,因为我也不熟悉。

  • 后端使用标准库或者第三库使用代码生成,这里我们主要介绍,第三方库:github.com/fogleman/gg
  • 后端生成HTML,再用 github.com/chromedp/chromedp 渲染HTML保存为图片。

Go gg 图片处理

第三方库:gg

gg 是一个用于在纯 Go 实现的用于渲染 2D 图形的库,包装的是标准库中的图片处理库。

对于图片处理,常见的需求生产带水印图片,图片裁剪,压缩图片大小。

简单例子

我们来看看 gg 这个库的简单入门怎么使用

package main
 
import (
    "fmt"
    "image/jpeg"
    "os"
 
    "github.com/fogleman/gg"
)
 
func main(){
 
    // 加载图片
    waterImage, err := gg.LoadPNG("test_pic.png")
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    // 通过图片实例化gg
    dc := gg.NewContextForImage(waterImage)
    dc.SetRGBA(1, 1, 1, 0)
 
    dc.SetRGB(0, 0, 0)
    // 加载字体,设置大小
    if err := dc.LoadFontFace("robotoLight.ttf", 16); err != nil {
        fmt.Println(err.Error())
        return
    }
    // 给图片添加文字,位置在(x, y) 处
    s := "hello worldhello"
    dc.DrawStringWrapped(s, 10, 25, 0, 0, float64(dc.Width())*0.9, 1.5, gg.AlignLeft)
    name := "/Users/yichuan.deng/Pictures/test_pic_1.png"
    newfile, err := os.Create(name)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    defer newfile.Close()
    // 将文件保存输出,并设置压缩比
    err = jpeg.Encode(newfile, dc.Image(), &jpeg.Options{Quality: 60})
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    return
}

上面的例子,是一个最简单的例子,做的事情很简单,就是往一张图片的坐标(10,25)处添加文字,在实际的开发过程中,我们还会遇到一个最典型的问题:能不能自动换行?

DrawString 这个方法中,并不能实现自动换行,超出图片宽度的数据就直接没了。。

自动换行

那么需要自动换行,文字对齐,我们应该怎么做呢?我们可以使用 DrawStringWrapped 方法,这个方法可以自动换行和对齐方式

DrawStringWrapped
s := "hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world"
// 每个参数的含义:(字符串,x,y,ax,ay,最大多宽开始换行,换行之后怎么对齐)
dc.DrawStringWrapped(s, 10, 25, 0, 0, float64(dc.Width())*0.9, 1.5, gg.AlignLeft)
// x,y,ax,ay 这四个参数有什么用?,DrawStringWrapped 这个函数会结合这四个参数来最终确认字符串应该写在什么坐标,具体为(x-w*ax,y-h*ay),w 为图片的宽度,h为图片的高度
// DrawStringWrapped 实现对齐的方式也是通过设置 (align Align)这个参数来改变 ax,ay的值来实现是左对齐或中间对齐或右对齐
 
type Align int
 
const (
    AlignLeft Align = iota  // 左对齐
    AlignCenter // 中间对齐
    AlignRight // 右对齐
)

其实这个函数:DrawStringWrapped 底层换行使用了他自己写的一个换行函数:wordWrap:先根据"\n"切分字符串,在根据“ ” 切分字符,保证按照单词切分,最后也是一个一个单词计算有没有超过最大宽度。

那么问题又来了,无论你的业务是在PC上,还是在APP上,你可以展示的图片大小一般是固定的,文字可以换行,但是又不能让他无限换行是吧?我们想设置让他只换多少行怎么设置呢?答案是没有!!!我们只有自己撸一个出来

自动撸自动换行
// truncateText 根据参数切分字符串
// 入参:
  // dc: gg的一个实例
  // originalText:原始的字符串
  // maxTextWidth: 最多宽换行
  // align:偏移量,有的字符串是从中间开始写的,所以这个align是针对从中间写的情况
// 返回值:切分之后的字符串
func truncateText(dc *gg.Context, originalText string, maxTextWidth float64, align float64) []string {
    result := make([]string, 0)
    lineNumber := 0
    // 最多只能换几行
    maxLine := 2
    w, _ := dc.MeasureString(originalText)
    // 增加2个像素是为了冗余
    if int(w) < int(maxTextWidth)+2 {
        return []string{originalText}
    }
    head := 0
    tail := 1
    add := false
    // 这个for循环就是为了一个一个字符技术,看在哪里可以切分
    for tail, _ = range originalText {
        s := originalText[head:tail]
        // 计算字符串的像素
        w, _ = dc.MeasureString(s)
        if w+align > maxTextWidth {
            add = true
            result = append(result, originalText[head:tail])
            lineNumber++
            head = tail
            // 计算是不是剩下的字符串不需再切分了,不需要则直接break循环
            w, _ = dc.MeasureString(originalText[tail:])
            if w < maxTextWidth {
                result = append(result, originalText[tail:])
                break
            }
        }
        // 到了最大的行数,则不在切分,直接追加到最后,这有可能最后一的一行如果超出了图片宽度,就会丢失数据
        if lineNumber >= maxLine {
            result = append(result, originalText[tail:])
            break
        }
    }
   // 完全没有切分过,把整个字符串返回
    if !add {
        result = append(result, originalText)
    }
    return result
}

我们自己撸的这个函数:truncateText 并没有完全参考源码中的:wordWrap,其实我们可以优化得再好一点,按照单词去切分和换行。

需要注意的点
增加水印

对图片自己水印是非常常见的需求,比如下方的图片,希望水印有透明而且旋转。但是对于gg这个库,我试过了没有太好的方式,所以,针对如果需要实现带水印的图片,可以事先生成好底图,然后再地图上增加文字。可以到达比较好的效果;
Go 图片生成处理_第1张图片

设置字体

设置字体我们使用 gg 的 LoadFontFace 方法,不过这方法每次都会去open这个字体文件,如果频繁生成图片,IO会对生成图片的性能产生较大的影响。

if err := dc.LoadFontFace("robotoLight.ttf", 16); err != nil {
    fmt.Println(err.Error())
    return
}

这个时候我们要避免重复打开这字体文件,我们需要为这个文件字体生成一个单例,只load一次这个字图文件就好了。具体做法如下

// 声明一个单例
var fontFace font.Face
 
// init 实例化一次
func init(){
    fontFace, err = gg.LoadFontFace("metadata/NotoSansCJKkr-Bold.ttf", points)
    if err != nil {
        panic(err)
    }
}
 
// 业务逻辑
func bizFunc(){
     // .......业务逻辑
     // 为gg实例dc设置字体
     dc.SetFontFace(fontFace)
     // .......业务逻辑
}
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
}

Go chromedp 图片处理

我们都知道,对于图片的处理,肯定是前端会更好处理,但是如果图片需要后端生成时,我们能不能通过直接用代码的方式生成图片,而是通过渲染HTML+CSS之后再浏览器上截图一样生成图片呢?

第三方库:chromedp 就提我们做了这样的事情,帮我买渲染出HTML并且截图保存为文件。图片就会变成使用了 第三方库:chromedp 来渲染HTML,这也有一个缺点,就是后端需要写HTML+CSS了。

第三方库:chromedp

例子
package main
 
import (
    "context"
    "io/ioutil"
    "log"
 
    "github.com/chromedp/chromedp"
)
 
func main() {
    // 生成 chrome 实例
    ctx, cancel := chromedp.NewContext(
        context.Background(),
        chromedp.WithDebugf(log.Printf),
    )
    defer cancel()
 
    // 设置截图某一个元素
    var buf []byte
    if err := chromedp.Run(ctx, elementScreenshot(`https://pkg.go.dev/`, `img.Homepage-logo`, &buf)); err != nil {
        log.Fatal(err)
    }
    if err := ioutil.WriteFile("/Users/yichuan.deng/Pictures/elementScreenshot.png", buf, 0o644); err != nil {
        log.Fatal(err)
    }
 
    // 全部截取,设置压缩比为90%
    if err := chromedp.Run(ctx, fullScreenshot(`https://brank.as/`, 90, &buf)); err != nil {
        log.Fatal(err)
    }
    if err := ioutil.WriteFile("/Users/yichuan.deng/Pictures/fullScreenshot.png", buf, 0o644); err != nil {
        log.Fatal(err)
    }
 
    log.Printf("wrote elementScreenshot.png and fullScreenshot.png")
}
 
// elementScreenshot 截图页面某一个元素
func elementScreenshot(urlstr, sel string, res *[]byte) chromedp.Tasks {
    return chromedp.Tasks{
        chromedp.Navigate(urlstr),
        chromedp.Screenshot(sel, res, chromedp.NodeVisible),
    }
}
 
// fullScreenshot 截图整个屏幕
func fullScreenshot(urlstr string, quality int, res *[]byte) chromedp.Tasks {
    return chromedp.Tasks{
        chromedp.Navigate(urlstr),
        chromedp.FullScreenshot(res, quality),
    }
}

参考

  • Go语言绘图】图片添加文字(一)
  • https://fonts.google.com/ 字体下载网站

总结

整体使用感觉来看,gg这个库还是差强人意,能够满足一些基本需求,再深入的话可能就得自己去实现了,毕竟Go只是通用语言,不可能把一些图片处理算法都给你实现好。
要实现复杂的图片处理需求,还是需要通过HTML+CSS+chromedp截图的方式来处理才是比较好的方案。而且这个库不紧紧可以将页面截图为图片,还可以变为PDF,更多的例子可以见:chromedp/examples

你可能感兴趣的:(Golang,go)