golang 防知乎 中文驗證碼 源碼

原創,轉載請註明出處!git

最開始用圖形來模仿文字進行各類角度的倒立和排列,後來切換爲文字後,有不少問題。總結以下:github

一、程序在畫圖形和畫文字方面不同,圖形的是從原點開始(0,0),而文字則從文字的基線開始(0,baseline)golang

二、在增長角度偏移時,文字或圖形的高寬會產生變化(偏∠45度時達到最大),這時候爲了讓它們頂點對齊,須要計算偏移量(用三角函數)app

三、在繪圖時,會先旋轉「畫布」(描述可能不許確),再繪製文字。此時要往回旋轉,不然下一個圖形會順着這個角度繼續畫。dom

四、爲了讓圖形保持固定寬度,對於有偏角的文字,須要平均縮小左右間距(不然不一樣的角度,固定的文字個數,會讓圖形寬度不一樣)函數

效果圖:字體

 

源碼:(代碼還能夠再整理和優化,但限於計劃時間,懶得弄了)優化

package main

import ( 
	"math"
	"reflect"
	"time"
	"math/rand"
	"github.com/golang/freetype/truetype"
	"github.com/fogleman/gg" // 須要安裝這個包
    "flag" 
    "fmt" 
    "io/ioutil" 
    "log" 
)

var wORDSLIB = []interface{}{"趙","錢","孫","李","周","吳","鄭","馮","陳","褚","衛","蔣","沈","韓","楊","朱","秦","尤","許","何","呂","施","張","孔","曹","嚴","華","金","魏","陶","姜","戚","謝","鄒","喻","柏","水","竇","章","雲","蘇","潘","葛","奚","範","彭","郎","魯","韋","昌","馬","苗","鳳","花","方","俞","任","袁","柳","酆","鮑","史","唐","費","廉","岑","薛","雷","賀","倪","湯","滕","殷","羅","畢","郝","鄔","安","常","樂","於","時","傅","皮","卞","齊","康","伍","餘","元","卜","顧","孟","平","黃","和","穆","蕭","尹","姚","邵","湛","汪","祁","毛","禹","狄","米","貝","明","臧","計","伏","成","戴","談","宋","茅","龐","熊","紀","舒","屈","項","祝","董","梁","杜","阮","藍","閔","席","季","麻","強","賈","路","婁","危","江","童","顏","郭","梅","盛","林","刁","鍾","徐","邱","駱","高","夏","蔡","樊","胡","凌","霍","虞","萬","支","柯","昝","管","盧","莫","經","房","裘","繆","幹","解","應","宗","丁","宣","賁","鄧","鬱","單","杭","洪","包","諸","左","石","崔","吉","鈕","龔","程","嵇","邢","滑","裴","陸","榮","翁","荀","羊","於","惠","甄","曲","家","封","芮","羿","儲","靳","汲","邴","糜","鬆","井","段","富","巫","烏","焦","巴","弓","牧","隗","山","谷","車","侯","宓","蓬","全","郗","班","仰","秋","仲","伊","宮","寧","仇","欒","暴","甘","鈄","厲","戎","祖","武","符","劉","景","詹","束","龍","葉","幸","司","韶","郜","黎","薊","薄","印","宿","白","懷","蒲","邰","從","鄂","索","鹹","籍","賴","卓","藺","屠","蒙","池","喬","陰","鬱","胥","能","蒼","雙","聞","莘","黨","翟","譚","貢","勞","逄","姬","扶","堵","冉","宰","酈","雍","卻","璩","桑","桂","濮","牛","壽","通","邊","扈","燕","冀","郟","浦","尚","農","溫","別","莊","晏","柴","瞿","閻","充","慕","連","茹","習","宦","艾","魚","容","向","古","易","慎","戈","廖","庾","終","暨","居","衡","步","都","耿","滿","弘","匡","國","文","寇","廣","祿","闕","東","歐","殳","沃","利","蔚","越","夔","隆","師","鞏","厙","聶","晁","勾","敖","融","冷","訾","辛","闞","那","簡","饒","空","曾","毋","沙","乜","養","鞠","須","豐","巢","關","蒯","相","查","後","荊","紅","遊","竺","權","逯","蓋","益","桓","公","俟","上","官","陽","人","赫","皇","甫","尉","遲","澹","臺","冶","政","淳","太","叔","申","軒","轅","令","狐","離","宇","長","鮮","閭","丘","徒","仉","督","子","顓","端","木","西","漆","雕","正","壤","駟","良","拓","跋","夾","父","晉","楚","閆","法","汝","鄢","塗","欽","百","裏","南","門","呼","延","歸","海","舌","微","生","嶽","帥","緱","亢","況","郈","有","琴","商","牟","佘","佴","伯","賞","墨","哈","譙","笪","年","愛","佟","第","五","言","福","姓"}

var ( 
    dpi      = flag.Float64("dpi", 72, "screen resolution in Dots Per Inch") 
    fontfile = flag.String("fontfile", "./SIMYOU.TTF", "filename of the ttf font") 
    size     = flag.Float64("size", 40, "font size in points") 
)

func main() { 
	x := "abc"
	fmt.Println(x[0:3])
    drawImageBygg()
} 

func drawImageBygg(){
	dc := gg.NewContext(320, 56)  // 56 => w*sin(45) + h*sin(45)  45度時,字體達到最大高度
	dc.SetRGBA(1, 1, 1,0) // 設置背景色:末尾爲透明度 1-0(1-不透明 0-透明)
	dc.Clear()
	dc.SetRGBA(0, 0, 0,1) // 設置字體色

	fontBytes, err := ioutil.ReadFile(*fontfile) 
    if err != nil { 
        log.Println(err) 
        return 
    } 
	font, err := truetype.Parse(fontBytes)
	if err != nil {
        log.Println(err) 
        return 
	}
	face := truetype.NewFace(font, &truetype.Options{
		Size: *size,
		DPI:*dpi,
	}) 
	dc.SetFontFace(face)

	// 初始化用於計算座標的變量
	fm := face.Metrics()
	ascent := float64(fm.Ascent.Round()) // 字體的基線到頂部距離
	decent := float64(fm.Descent.Round()) // 字體的基線到底部的距離
	w := float64(fm.Height.Round()) // 方塊字,大多數應爲等寬字,即和高度同樣
	h := float64(fm.Height.Round())
	totalWidth := 0.0 // 目前已累積的圖片寬度(須要用來計算字體位置)

	// 隨機取漢字,定位倒立的字
	words := getRandomMembersFromMemberLibary(wORDSLIB,8) // 取8個字
	reverseWordsIndex := getRandomMembersFromMemberLibary([]interface{}{0,1,2,3,4,5,6,7},2) // 隨機2個倒立字

	for i,word := range words{
		degree := If(Contain(i,reverseWordsIndex),float64(RandInt64(150,210)),float64(RandInt64(-30,30))) // 隨機角度,正向角度 -30~30,倒立角度 150~210
		x,y,leftCutSize,rightCS := getCoordByQuadrantAndDegree(w,h,ascent,decent,degree,totalWidth)
		dc.RotateAbout(gg.Radians(degree),0,0)
		dc.DrawStringAnchored(word.(string), x,y, 0,0)
		dc.RotateAbout(-1*gg.Radians(degree),0,0)
		totalWidth = totalWidth + leftCutSize + rightCS
		fmt.Println("x:",x,"y:",y,"total:",totalWidth,"degree:",degree)
	}

	dc.Stroke()
	dc.SavePNG("out.png")
    fmt.Println("Wrote out.png OK.") 
}

func getCoordByQuadrantAndDegree(w,h,ascent,descent,degree,beforTotalWidth float64)(x,y,leftCutSize,rightCutSize float64){
	var totalWidth float64
	switch{
		case degree<=0 && degree >= -40:  // 第一象限:逆時針 -30度 ~ 0  <=>  330 ~ 360 (目前參數要傳入負數)  
			rd := -1 * degree // 轉爲正整數,便於計算
			leftCutSize = w*getDegreeSin(90-rd)
			rightCutSize = h*getDegreeSin(rd)
			
			offset := (leftCutSize + rightCutSize - w) / 2 // 橫向偏移量(角度傾斜越厲害,佔寬越多,經過偏移量分攤給它的左右邊距來收窄)
			leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset

			totalWidth = beforTotalWidth + leftCutSize 
			x = getDegreeSin(90 - rd)*totalWidth - w 
			y = ascent + getDegreeSin(rd)*totalWidth  
		case degree >=0 && degree <= 40:  // 第四象限:順時針 0 ~ 30度
			leftCutSize = h*getDegreeSin(degree)
			rightCutSize = w*getDegreeSin(90-degree)

			offset := (leftCutSize + rightCutSize - w) / 2
			leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset

			totalWidth = beforTotalWidth + leftCutSize // 如今totalwidth = 前面的寬 + 本身的左切邊
			x = getDegreeSin(90-degree)*totalWidth
			y = ascent - getDegreeSin(degree)*totalWidth 
		case degree >= 180 && degree <= 220:  // 第二象限:順時針 180 ~ 210度
			rd := degree - 180
			leftCutSize = h*getDegreeSin(rd)
			rightCutSize = w*getDegreeSin(90-rd)

			offset := (leftCutSize + rightCutSize - w) / 2 
			leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset

			totalWidth = beforTotalWidth + leftCutSize 
			x = -1 * (getDegreeSin(90-rd)*totalWidth + w)
			y = getDegreeSin(rd)*totalWidth - descent
		case degree >= 140 && degree <= 180:  // 第三象限:順時針 150 ~ 180度
			rd := 180-degree
			leftCutSize = w*getDegreeSin(90-rd)
			rightCutSize = h*getDegreeSin(rd)

			offset := (leftCutSize + rightCutSize - w) / 2
			leftCutSize,rightCutSize = leftCutSize - offset,rightCutSize - offset
			
			totalWidth = beforTotalWidth + leftCutSize 
			x = -1 * (getDegreeSin(90-rd) * totalWidth)
			y = -1 * (getDegreeSin(rd) * totalWidth + descent)
		default: panic(fmt.Sprintf("非法的參數:%f",degree))
	}
	return
}

func getDegreeSin(degree float64) float64{
	return math.Sin(degree*math.Pi/180)
}

//RandInt64 ...
func RandInt64(min, max int64) int64 {
	if min >= max || min == 0 || max == 0 {
		return max
	}
	rand.Seed(time.Now().UnixNano())
	return rand.Int63n(max-min) + min
}

func getRandomMembersFromMemberLibary(lib []interface{},size int)[]interface{}{
	source,result := make([]interface{},0),make([]interface{},0)
	if size <= 0 || len(lib) == 0{
		return result
	}
	for _,v := range lib{
		source = append(source,v)
	}
	if size >= len(lib){
		return source
	}
	for i:=0;i<size;i++{
		rand.Seed(time.Now().UnixNano())
		pos := rand.Intn(len(source))
		result = append(result,source[pos])
		source = append(source[:pos], source[pos+1:]...)
	}
	return result
}

//Contain ...
func Contain(obj interface{}, target interface{}) bool {
    targetValue := reflect.ValueOf(target)
    switch reflect.TypeOf(target).Kind() {
    case reflect.Slice, reflect.Array:
        for i := 0; i < targetValue.Len(); i++ {
            if targetValue.Index(i).Interface() == obj {
                return true
            }
        }
    case reflect.Map:
        if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() {
            return true
        }
    }

    return false
}

//If ...
func If(expr bool,trueVal float64,falseVal float64) float64{
	if expr {
		return trueVal 
	}
	return falseVal
}

  

 須要一個字體文件,這裏使用的幼圓(幼圓免費,其餘免費字體懶得找)。spa

注意:字體不一樣,寬度和高度可能會須要調整。blog

相關文章
相關標籤/搜索