開發(fā)過(guò)程會(huì)有使用go繪制較為復(fù)雜圖形的需求思杯,比如這次寥茫,需要做一個(gè)類似拖拽圖案驗(yàn)證碼。
具體需求:
- 4條邊,凹凸平3個(gè)形狀边锁,隨機(jī)組合姑食,
- 隨機(jī)位置上截圖如上形狀的圖片,并且在原圖有如上形狀的陰影遮罩
go自帶image/draw
包不能繪制復(fù)雜的圖形(或者說(shuō)太復(fù)雜)茅坛,經(jīng)過(guò)嘗試音半,可以使用go來(lái)繪制svg,再使用imagemagick來(lái)進(jìn)行特效處理贡蓖,最后再次使用go把圖層融合在一起曹鸠。
github.com/ajstarks/svgo
#基本圖形svg繪制
github.com/gographics/imagick
#特效處理(需安裝imagemagick)
先上最終最終效果
go 繪制基本圖形
使用 svgo 來(lái)繪制 path,生成 svg斥铺,代碼貼在最后
imagemagick convert把 svg 轉(zhuǎn)化為 png, 加陰影
注:只展示 imagemagick convert彻桃,沒有融合到 go 程序里
convert \
\( -background none MSVG:tile-cut.svg -background black -shadow 50x2+0+10 \
-gravity North -background none -extent 200x200 \
-compose over \
\( -background none MSVG:tile-cut.svg -background white -shadow 120x6+0+0 -resize 95% \
-gravity center -background none -extent 200x200 \
\) \
-background none -gravity NorthEast -geometry +0+0 -composite \
\) \
-compose over tile-line.svg -gravity center -geometry +0+0 -composite \
tile.png
代碼注釋:
- MSVG 支持svg 作為輸入源,需要 imagemagick 支持 rsvg晾蜘, 比如mac:
brew install imagemagick --with-librsvg
-
-background black -shadow 50x2+0+10
前兩個(gè)數(shù)字自己調(diào)著看邻眷,影響shadow的大小和透明度眠屎,后兩個(gè)是x,y軸偏移肆饶,可+可- -
-background none
背景透明 - compose 兩個(gè)圖層組合方式改衩, over 表示 后者在前者之上,還有其他選項(xiàng)驯镊,詳見官網(wǎng)手冊(cè)
-
-gravity North -background none -extent 200x200
-extent 擴(kuò)展圖片尺寸葫督,-gravity North 表示擴(kuò)展的時(shí)候,原圖處于什么位置阿宅,North 表示北候衍,還有NorthEast 等,詳見官網(wǎng)手冊(cè)
處理陰影svg
convert -density 2400 -background transparent MSVG:tile-shadow.svg \
-fill "rgba(0,0,0,0.3)" -opaque black -resize 200x200 tile-shadow.png
代碼注釋:
- -opaque 刪除圖片中顏色洒放,這里刪除黑色,填充rgba(0,0,0,0.3), 否則不能轉(zhuǎn)化透明度滨砍,這里用了一個(gè)trick
- -density 2400 這里通過(guò)修改了分辨率來(lái)提高 svg 轉(zhuǎn)化到 png 的細(xì)致程度往湿,再?gòu)?qiáng)制 -resize 200x200
完成這個(gè)小需求繞挺多彎路現(xiàn)總結(jié)如下:
- go 可以完成一些簡(jiǎn)單圖形的繪制,比如shape/tile 中At方法其實(shí)是對(duì)每個(gè)點(diǎn)進(jìn)行上色惋戏,這是 draw 包的實(shí)現(xiàn)
- go 支持圖層疊加领追,支持圖層融合
- 繪制幾何圖形使用 svg path,很方便响逢,go可以使用 svgo 包绒窑,但是不支持轉(zhuǎn)化成png圖片,也不能作為draw包的輸入源
- 圖片處理 使用 imagemagick, 我這里沒有使用擴(kuò)展把邏輯寫到代碼里舔亭,因?yàn)槲铱梢园堰@些 tile png先生成些膨,go直接疊加圖層,降低運(yùn)算钦铺,比如這次需求订雾,4條邊,3種形狀(凹凸平),2個(gè)類型(外邊陰影矛洞,陰影填充) 洼哎,3x3x3x3x2 = 162
附go代碼:
main.go
package main
import (
"image"
"log"
_ "image/jpeg"
_ "image/png"
"os"
"math/rand"
"time"
"math"
"image/png"
"image/draw"
"captcha/shape"
)
func main() {
//保存svgs,
saveSvgs()
cutWidth := 200
top, right, bottom, left := 1, 1, 0, -1
reader, err := os.Open("images/14d4c647b216975cb298481f4e550ebc.jpg")
if err != nil {
log.Fatal(err)
}
defer reader.Close()
m, _, err := image.Decode(reader)
rgbImg := m.(*image.YCbCr)
reader2, err := os.Open("tile.png")
if err != nil {
log.Fatal(err)
}
defer reader2.Close()
m2, _, err := image.Decode(reader2)
rgbImg2 := m2.(*image.NRGBA64)
reader3, err := os.Open("tile-shadow.png")
if err != nil {
log.Fatal(err)
}
defer reader3.Close()
m3, _, err := image.Decode(reader3)
rgbImg3 := m3.(*image.NRGBA64)
randRect := getRandomRectangle(m.Bounds(), cutWidth, cutWidth)
tile := shape.Tile{image.Pt(45, 0), cutWidth, top, right, bottom, left, false}
// go draw 包處理代碼沼本,
// 新建圖片
originImage := image.NewRGBA(rgbImg.Bounds())
// 使用draw原圖
draw.Draw(originImage, rgbImg.Bounds(), rgbImg, rgbImg.Bounds().Min, draw.Src)
// 把imagemagick處理好的陰影png 繪制到圖片的隨機(jī)位置 randRect 之上
draw.Draw(originImage, randRect.Bounds(), rgbImg3, rgbImg3.Bounds().Min, draw.Over)
f2, _ := os.Create("origin.png")
defer f2.Close()
png.Encode(f2, originImage)
cutImage4 := image.NewRGBA(image.Rect(0, 0, cutWidth, cutWidth))
// 在隨機(jī)位置randRect 截取 tile 形狀的截圖
draw.DrawMask(cutImage4, cutImage4.Bounds(), rgbImg, randRect.Bounds().Min, &tile, tile.Bounds().Min, draw.Src)
// 在圖片之上添加 imagemagick 處理好的 tile.png
draw.Draw(cutImage4, cutImage4.Bounds(), rgbImg2, rgbImg2.Bounds().Min, draw.Over)
f5, _ := os.Create("tile-final.png")
defer f5.Close()
png.Encode(f5, cutImage4)
}
// 繪制svg
func saveSvgs(){
cutWidth := 200
top, right, bottom, left := 1, 1, 0, -1
//保存 svg 文件
tileSvg := shape.TileSvg{cutWidth, top, right, bottom, left}
tileSvg.SaveSvg("tile-cut.svg", "stroke-width:10;stroke:White;fill:none;")
tileSvg.SaveSvg("tile-line.svg", "stroke-width:2;stroke:White;fill:none;")
tileSvg.SaveSvg("tile-shadow.svg", "stroke-width:2;stroke:White;fill:black;fill-opacity:0.5")
}
// 隨機(jī)獲取截圖位置
func getRandomRectangle(rectangle image.Rectangle, subWidth int, subHeight int) (*image.Rectangle) {
bounds := rectangle.Bounds()
width := bounds.Max.X - bounds.Min.X
height := bounds.Max.Y - bounds.Min.Y
rand.Seed(time.Now().Unix())
maxPosX := math.Ceil(float64(width)*0.7) - math.Ceil(float64(subWidth)*1.3)
maxPosY := math.Ceil(float64(height)*0.7) - math.Ceil(float64(subHeight)*1.3)
if maxPosX < 0 || maxPosY < 0 {
//todo error
tmp := image.Rect(0, 0, 0, 0)
return &tmp
}
posX := rand.Intn(int(maxPosX)) + int(math.Ceil(float64(width)*0.3))
posY := rand.Intn(int(maxPosY)) + int(math.Ceil(float64(height)*0.3))
log.Println(maxPosX, maxPosY)
log.Println(posX, posY, posX+int(subWidth), posY+int(subHeight))
retRectangle := image.Rect(posX, posY, posX+int(subWidth), posY+int(subHeight), )
return &retRectangle
}
shape/tilesvg.go 繪制svg圖形
package shape
import (
"os"
"fmt"
"github.com/ajstarks/svgo"
)
type TileSvg struct {
Width int
T int
R int
B int
L int
}
/**
shape svg path
*/
func (t *TileSvg) SaveSvg(filename string, stroke string) {
width := t.Width
height := t.Width
f5, _ := os.Create(filename)
canvas := svg.New(f5)
canvas.Start(width, height)
if len(stroke) <= 0 {
stroke = "stroke-width:10;stroke:green;fill:red;"
}
path := fmt.Sprintf("M%d,%d h%d ", width/5, width/5, width/5)
if t.T > 0 {
path += fmt.Sprintf("A%d,%d 0 0,1 %d,%d ", width/10, width/10, width*3/5, width/5)
} else if t.T < 0 {
path += fmt.Sprintf("A%d,%d 0 1,0 %d,%d ", width/10, width/10, width*3/5, width/5)
} else {
path += fmt.Sprintf("h+%d ", width/5)
}
path += fmt.Sprintf("h%d v%d ", width/5, width/5)
if t.R > 0 {
path += fmt.Sprintf("A%d,%d 0 0,1 %d,%d ", width/10, width/10, width*4/5, width*3/5)
} else if t.R < 0 {
path += fmt.Sprintf("A%d,%d 0 1,0 %d,%d ", width/10, width/10, width*4/5, width*3/5)
} else {
path += fmt.Sprintf("v+%d ", width/5)
}
path += fmt.Sprintf("v%d h-%d ", width/5, width/5)
if t.B > 0 {
path += fmt.Sprintf("A%d,%d 0 0,1 %d,%d ", width/10, width/10, width*2/5, width*4/5)
} else if t.B < 0 {
path += fmt.Sprintf("A%d,%d 0 1,0 %d,%d ", width/10, width/10, width*2/5, width*4/5)
} else {
path += fmt.Sprintf("h-%d ", width/5)
}
path += fmt.Sprintf("h-%d v-%d ", width/5, width/5)
if t.L > 0 {
path += fmt.Sprintf(" A%d,%d 0 0,1 %d,%d ", width/10, width/10, width/5, width*2/5)
} else if t.L < 0 {
path += fmt.Sprintf(" A%d,%d 0 1,0 %d,%d ", width/10, width/10, width/5, width*2/5)
} else {
path += fmt.Sprintf("v-%d ", width/5)
}
path += fmt.Sprintf("v-%d ", width/5)
canvas.Path(path, stroke)
canvas.End()
}
shape/tile.go
package shape
import (
"image"
"image/color"
)
type Tile struct {
Min image.Point
Width int
T int
R int
B int
L int
Revert bool
}
func (c *Tile) ColorModel() color.Model {
return color.AlphaModel
}
func (c *Tile) Bounds() image.Rectangle {
return image.Rect(c.Min.X, c.Min.Y, c.Min.X+c.Width, c.Min.Y+c.Width)
}
func (c *Tile) At(x, y int) color.Color {
colorAt := color.Alpha{220}
colorWithin := color.Alpha{220}
colorWithout := color.Alpha{0}
if c.Revert {
colorWithin = color.Alpha{0}
colorWithout = color.Alpha{220}
}
margin := c.Width / 5
if x > (c.Min.X+c.Width/5) && x < (c.Bounds().Max.X-c.Width/5) && y > (c.Min.Y+c.Width/5) && y < (c.Bounds().Max.Y-c.Width/5) {
colorAt = colorWithin
} else {
colorAt = colorWithout
}
r := c.Width / 10
//todo 優(yōu)化算法
if c.T != 0 {
roundSpot := image.Pt((c.Bounds().Max.X-c.Min.X)/2+c.Min.X, c.Min.Y+margin)
xx, yy, rr := float64(x-roundSpot.X)+0.5, float64(y-roundSpot.Y)+0.5, float64(r)+0.5
if xx*xx+yy*yy < rr*rr {
if c.T > 0 {
colorAt = colorWithin
} else {
colorAt = colorWithout
}
}
}
if c.R != 0 {
roundSpot := image.Pt(c.Bounds().Max.X-margin, (c.Bounds().Max.Y-c.Min.Y)/2+c.Min.Y)
xx, yy, rr := float64(x-roundSpot.X)+0.5, float64(y-roundSpot.Y)+0.5, float64(r)+0.5
if xx*xx+yy*yy < rr*rr {
if c.R > 0 {
colorAt = colorWithin
} else {
colorAt = colorWithout
}
}
}
if c.B != 0 {
roundSpot := image.Pt((c.Bounds().Max.X-c.Min.X)/2+c.Min.X, (c.Bounds().Max.Y - margin))
xx, yy, rr := float64(x-roundSpot.X)+0.5, float64(y-roundSpot.Y)+0.5, float64(r)+0.5
if xx*xx+yy*yy < rr*rr {
if c.B > 0 {
colorAt = colorWithin
} else {
colorAt = colorWithout
}
}
}
if c.L != 0 {
roundSpot := image.Pt(c.Min.X+margin, (c.Bounds().Max.Y-c.Min.Y)/2+c.Min.Y)
xx, yy, rr := float64(x-roundSpot.X)+0.5, float64(y-roundSpot.Y)+0.5, float64(r)+0.5
if xx*xx+yy*yy < rr*rr {
if c.L > 0 {
colorAt = colorWithin
} else {
colorAt = colorWithout
}
}
}
return colorAt
}