go+imagemagick使用svg+png繪制復(fù)雜圖片

開發(fā)過(guò)程會(huì)有使用go繪制較為復(fù)雜圖形的需求思杯,比如這次寥茫,需要做一個(gè)類似拖拽圖案驗(yàn)證碼。
具體需求:

  1. 4條邊,凹凸平3個(gè)形狀边锁,隨機(jī)組合姑食,
  2. 隨機(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)

先上最終最終效果

origin.png
tile-final.png

go 繪制基本圖形

使用 svgo 來(lái)繪制 path,生成 svg斥铺,代碼貼在最后

imagemagick convert把 svg 轉(zhuǎn)化為 png, 加陰影

注:只展示 imagemagick convert彻桃,沒有融合到 go 程序里

tile.png
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

代碼注釋:

  1. MSVG 支持svg 作為輸入源,需要 imagemagick 支持 rsvg晾蜘, 比如mac:
    brew install imagemagick --with-librsvg
  2. -background black -shadow 50x2+0+10 前兩個(gè)數(shù)字自己調(diào)著看邻眷,影響shadow的大小和透明度眠屎,后兩個(gè)是x,y軸偏移肆饶,可+可-
  3. -background none 背景透明
  4. compose 兩個(gè)圖層組合方式改衩, over 表示 后者在前者之上,還有其他選項(xiàng)驯镊,詳見官網(wǎng)手冊(cè)
  5. -gravity North -background none -extent 200x200 -extent 擴(kuò)展圖片尺寸葫督,-gravity North 表示擴(kuò)展的時(shí)候,原圖處于什么位置阿宅,North 表示北候衍,還有NorthEast 等,詳見官網(wǎng)手冊(cè)

處理陰影svg

tile-shadow.png
convert -density 2400 -background transparent  MSVG:tile-shadow.svg \
-fill "rgba(0,0,0,0.3)" -opaque black -resize 200x200 tile-shadow.png

代碼注釋:

  1. -opaque 刪除圖片中顏色洒放,這里刪除黑色,填充rgba(0,0,0,0.3), 否則不能轉(zhuǎn)化透明度滨砍,這里用了一個(gè)trick
  2. -density 2400 這里通過(guò)修改了分辨率來(lái)提高 svg 轉(zhuǎn)化到 png 的細(xì)致程度往湿,再?gòu)?qiáng)制 -resize 200x200

完成這個(gè)小需求繞挺多彎路現(xiàn)總結(jié)如下:

  1. go 可以完成一些簡(jiǎn)單圖形的繪制,比如shape/tile 中At方法其實(shí)是對(duì)每個(gè)點(diǎn)進(jìn)行上色惋戏,這是 draw 包的實(shí)現(xiàn)
  2. go 支持圖層疊加领追,支持圖層融合
  3. 繪制幾何圖形使用 svg path,很方便响逢,go可以使用 svgo 包绒窑,但是不支持轉(zhuǎn)化成png圖片,也不能作為draw包的輸入源
  4. 圖片處理 使用 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
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末噩峦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子抽兆,更是在濱河造成了極大的恐慌识补,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件郊丛,死亡現(xiàn)場(chǎng)離奇詭異李请,居然都是意外死亡瞧筛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門导盅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)较幌,“玉大人,你說(shuō)我怎么就攤上這事白翻≌” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵滤馍,是天一觀的道長(zhǎng)岛琼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)巢株,這世上最難降的妖魔是什么槐瑞? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮阁苞,結(jié)果婚禮上困檩,老公的妹妹穿的比我還像新娘。我一直安慰自己那槽,他們只是感情好悼沿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著骚灸,像睡著了一般糟趾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上甚牲,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天义郑,我揣著相機(jī)與錄音,去河邊找鬼鳖藕。 笑死魔慷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的著恩。 我是一名探鬼主播院尔,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼喉誊!你這毒婦竟也來(lái)了邀摆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤伍茄,失蹤者是張志新(化名)和其女友劉穎栋盹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體敷矫,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡例获,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年汉额,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榨汤。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蠕搜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出收壕,到底是詐尸還是另有隱情妓灌,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布蜜宪,位于F島的核電站虫埂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏圃验。R本人自食惡果不足惜掉伏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望损谦。 院中可真熱鬧岖免,春花似錦、人聲如沸照捡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)栗精。三九已至,卻和暖如春瞻鹏,著一層夾襖步出監(jiān)牢的瞬間悲立,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工新博, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留薪夕,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓赫悄,卻偏偏與公主長(zhǎng)得像原献,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子埂淮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容