雖然 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 標準庫中缺少需要的格式支持,我們可以通過 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
}
縮放圖片
截止 go1.19 image 標準庫中仍然沒有縮放圖片的功能,我們可以使用 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) // 繪製第二幅圖
上述代碼實現了將兩張圖片拼接成在一起的效果:
其核心是 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 函數的參數如下:
- dst: 繪圖的畫布,只能是 *image.RGBA 或 *image.Paletted 類型
- r: dst 上繪圖範圍
- src: 要在畫布上繪製的圖像
- sp: src 的起始點,實際繪製的圖像是
img.SubImage(image.Rectangle{Min: sp, Max: src.Bounds().Max})
。注意,src 爲 SubImage 時,sp 應爲 src.Bounds().Min - op: porter-duff 混合方式, 具體介紹可以看下面的微軟的參考資料。image/draw 庫提供了 draw.Src 和 draw.Over 兩種混合方式
- draw.Src: 將 src 覆蓋在 dst 上
- draw.Over: src 在上,dst 在下按照 alpha 值進行混合。在圖片完全不透明時,draw.Over 與 draw.Src 沒有區別
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 模式進行混合
順便把頭像畫在圖片上:
圓角
圓角的實現原理和圓形一樣,改一下 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
}