虽然 Go 语言主要用于 Web 后端以及各类中间件和基础设施开发,也难免遇到一些图像处理的需求。Go 语言提供的 image 标准库提供了基本的图片加载、裁剪、绘制等能力,可以帮助我们实现一些绘图需求。

加载图片

image.Decode(io.Reader) 会从 reader 获取数据,并根据文件开头的 Magic Number 来选择合适的解码器:

import (
	"fmt"
	"image"
	_ "image/jpeg" // 通过 jpeg 包中的 init 函数注册解码器
	_ "image/png"
	"os"
)

func main() {
	input, _ := os.Open("avatar.jpeg")
	defer input.Close()
	img, _, err := image.Decode(input)
	if err != nil {
		panic(err)
	}
	fmt.Println(img.Bounds())
}

在知道图片类型的情况下也可以直接使用相应的解码器:

import (
	"fmt"
	"image/jpeg"
	"os"
)

func main() {
	input, _ := os.Open("avatar.jpeg")
	defer input.Close()
	img, err := jpeg.Decode(input)
	if err != nil {
		panic(err)
	}
	fmt.Println(img.Bounds())
}

Decode 返回值类型为 image.Image, 它是 image 库定义的一个接口:

type Image interface {
  ColorModel() color.Model // 返回图片的颜色模型, 如 RGB、YUV
  Bounds() Rectangle       // 返回图片的长宽
  At(x, y int) color.Color // 返回(x,y)像素点的颜色
}
image.RegisterFormat
保存图片

保存图片与导入图片类似, 将图像和 io.Writer 传给 png 或 jpeg 编码器即可:

func saveImage(img image.Image, filename string) error {
	outFile, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer outFile.Close()
	b := bufio.NewWriter(outFile)
	err = jpeg.Encode(b, img, nil)
	if err != nil {
		return err
	}
	err = b.Flush()
	if err != nil {
		return err
	}
	return nil
}
裁剪图片

图片的裁剪主要使用 SubImage() 方法:

width := 540
height := 960
window := image.Rect(
			(img.Bounds().Dx()-width)/2, 0,
			(img.Bounds().Dx()+width)/2, height,
		)
return img.SubImage(window)

裁剪横屏图片中央 540*960 的范围,注意 SubImage 左上角的坐标不是 (0,0) 而是在原图片中的坐标(即 window.Min)。

subImage 使用 image.Rectangle 结构体表示的矩形范围, 它通过左上角和右下角坐标来描述矩形。坐标 0 点再原图左上角,X 轴向右 Y 轴向下。

type Rectangle struct {
	Min, Max Point
}

type Point struct {
	X, Y int
}
缩放图片
golang.org/x/image
dst := image.NewRGBA(image.Rect(0, 0, src.Bounds().Max.X/2, src.Bounds().Max.Y/2)) // 缩放后的目标图片
draw.NearestNeighbor.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil) // 使用 NearestNeighbor 算法进行伸缩

x/image 包中有四种缩放算法:

  • NearestNeighbor
  • ApproxBiLinear
  • BiLinear
  • CatmullRom
github.com/nfnt/resize
resize.Resize(targetWidth, targetHeight, img, resize.NearestNeighbor)
绘制纯色图片

image.Uniform 是 image 库中纯色图片类型。下面的代码生成了一张纯蓝色的图片:

m := image.NewRGBA(image.Rect(0, 0, 640, 480))
blue := color.RGBA{0, 0, 255, 255}
draw.Draw(m, m.Bounds(), &image.Uniform{blue}, image.ZP, draw.Src)

go 标准库并不能解析我们常用的 16 进制的色号(示例:#D34899),可以使用 https://github.com/icza/gox 提供的解析函数:

func ParseHexColor(s string) (c color.RGBA, err error) {
	c.A = 0xff

	if s[0] != '#' {
		return c, errInvalidFormat
	}

	hexToByte := func(b byte) byte {
		switch {
		case b >= '0' && b <= '9':
			return b - '0'
		case b >= 'a' && b <= 'f':
			return b - 'a' + 10
		case b >= 'A' && b <= 'F':
			return b - 'A' + 10
		}
		err = errInvalidFormat
		return 0
	}

	switch len(s) {
	case 7:
		c.R = hexToByte(s[1])<<4 + hexToByte(s[2])
		c.G = hexToByte(s[3])<<4 + hexToByte(s[4])
		c.B = hexToByte(s[5])<<4 + hexToByte(s[6])
	case 4:
		c.R = hexToByte(s[1]) * 17
		c.G = hexToByte(s[2]) * 17
		c.B = hexToByte(s[3]) * 17
	default:
		err = errInvalidFormat
	}
	return
}
自由绘制

jpeg 解码器返回的 image 对象(姑且称为对象)是只读的并不能在上面自由绘制,我们需要创建一个画布:

width := 1080
height := 1920
dst := image.NewRGBA(image.Rect(0, 0, width, height)) // 创建一块画布
draw.Draw(dst, image.Rect(0, height/4, width/2, 3*height/4), images[0], image.Pt(0, 0), draw.Over) // 绘制第一幅图
draw.Draw(dst, image.Rect(width/2, height/4, width, 3*height/4), images[1], image.Pt(0, 0), draw.Over) // 绘制第二幅图

上述代码实现了将两张图片拼接成在一起的效果:

result.jpg

其核心是 image/draw 中的 Draw 函数,它的定义如下:

func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op) {
	DrawMask(dst, r, src, sp, nil, image.Point{}, op)
}

func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point, mask image.Image, mp image.Point, op Op)

Draw 函数的参数如下:

img.SubImage(image.Rectangle{Min: sp, Max: src.Bounds().Max})

Draw 不支持设置背景图片或者背景色,其实只要在画布最下层绘制一张和画布一样大的图片或纯色图片即可。

遮罩

DrawMask 函数可以在 src 上面一个遮罩,可以实现圆形图片、圆角等效果。

圆形图片

首先定义一个中心圆形不透明、边缘部分透明的 circle 类型,实现 image.Image 接口:

// 圆形遮罩
type circle struct {
	p image.Point // 圆心位置
	r int // 半径
}

func (c *circle) ColorModel() color.Model {
	return color.AlphaModel
}

func (c *circle) Bounds() image.Rectangle {
	return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r)
}

func (c *circle) At(x, y int) color.Color {
	xx, yy, rr := float64(x-c.p.X)+0.5, float64(y-c.p.Y)+0.5, float64(c.r)
	if xx*xx+yy*yy < rr*rr {
		return color.Alpha{A: 255} // 半径以内的图案设成完全不透明
	}
	return color.Alpha{}
}

使用 DrawMask 方法将其绘制出来:

c := circle{p: image.Point{X: avatarRad, Y: avatarRad}, r: avatarRad}
circleAvatar := image.NewRGBA(image.Rect(0, 0, avatarRad*2, avatarRad*2)) // 准备画布
draw.DrawMask(circleAvatar, circleAvatar.Bounds(), avatar, image.Point{}, &c, image.Point{}, draw.Over) // 使用 Over 模式进行混合

avatar.jpeg

circle0.jpg

顺便把头像画在图片上:

card0.jpg

圆角

圆角的实现原理和圆形一样,改一下 At 的函数公式即可:

type radius struct {
  p image.Point // 矩形右下角位置
  r int
}
​
func (c *radius) ColorModel() color.Model {
  return color.AlphaModel
}
​
func (c *radius) Bounds() image.Rectangle {
  return image.Rect(0, 0, c.p.X, c.p.Y)
}
​
// 对每个像素点进行色值设置,分别处理矩形的四个角,在四个角的内切圆的外侧,色值设置为全透明,其他区域不透明
func (c *radius) At(x, y int) color.Color {
  var xx, yy, rr float64
  var inArea bool
  // left up
  if x <= c.r && y <= c.r {
    xx, yy, rr = float64(c.r-x)+0.5, float64(y-c.r)+0.5, float64(c.r)
    inArea = true
  }
  // right up
  if x >= (c.p.X-c.r) && y <= c.r {
    xx, yy, rr = float64(x-(c.p.X-c.r))+0.5, float64(y-c.r)+0.5, float64(c.r)
    inArea = true
  }
  // left bottom
  if x <= c.r && y >= (c.p.Y-c.r) {
    xx, yy, rr = float64(c.r-x)+0.5, float64(y-(c.p.Y-c.r))+0.5, float64(c.r)
    inArea = true
  }
  // right bottom
  if x >= (c.p.X-c.r) && y >= (c.p.Y-c.r) {
    xx, yy, rr = float64(x-(c.p.X-c.r))+0.5, float64(y-(c.p.Y-c.r))+0.5, float64(c.r)
    inArea = true
  }
​
  if inArea && xx*xx+yy*yy >= rr*rr {
    return color.Alpha{}
  }
  return color.Alpha{A: 255}
}
添加文字
github.com/golang/freetype
img := image.NewRGBA(image.Rect(0, 0, width, height))

ttfBytes, err := ioutil.ReadFile(fontSource) // 读取 ttf 文件
if err != nil {
    return err
}

font, err := freetype.ParseFont(ttfBytes)
if err != nil {
    return err
}

fc := freetype.NewContext()
fc.SetDPI(72) // 每英寸的分辨率
fc.SetFont(font) 
fc.SetFontSize(size)
fc.SetClip(img.Bounds())
fc.SetDst(img)
fc.SetSrc(image.Black) // 设置绘制操作的源图像,通常使用纯色图片 image.Uniform 

_, err = fc.DrawString("hello world", freetype.Pt(0, 0))
if err != nil {
    return err
}