dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

image.go (21492B)


      1 package captcha
      2 
      3 import (
      4 	"bytes"
      5 	"dkforest/bindata"
      6 	"github.com/fogleman/gg"
      7 	"github.com/golang/freetype/truetype"
      8 	"github.com/sirupsen/logrus"
      9 	font1 "golang.org/x/image/font"
     10 	"image"
     11 	"image/color"
     12 	"image/png"
     13 	"io"
     14 	"math"
     15 	"math/rand"
     16 	"sync"
     17 )
     18 
     19 type Image struct {
     20 	*image.RGBA
     21 	rnd              *rand.Rand
     22 	imageWidth       int
     23 	imageHeight      int
     24 	numWidth         int
     25 	numHeight        int
     26 	nbDigits         int
     27 	digitSpacing     int
     28 	digitVertSpacing int
     29 	borderTop        int
     30 	borderLeft       int
     31 	nbNumRows        int
     32 	nbNumCols        int
     33 	centerOffset     float64
     34 	numbersMatrix    [][]Number
     35 	mainPath         Path
     36 	secondPath       Path
     37 	thirdPath        Path
     38 	displayDebug     bool
     39 	renderHelpImg    bool
     40 	difficulty       int64
     41 	c                *gg.Context
     42 	CubicDelta       float64
     43 }
     44 
     45 type Number struct {
     46 	Num     uint8
     47 	Angle   float64
     48 	FaceIdx int
     49 	Face    font1.Face
     50 	Opacity uint8
     51 }
     52 
     53 type iPoint interface {
     54 	GetX() float64
     55 	GetY() float64
     56 }
     57 
     58 // To find orientation of ordered triplet (p, q, r).
     59 // The function returns following values
     60 // 0 --> p, q and r are collinear
     61 // 1 --> Clockwise
     62 // 2 --> Counterclockwise
     63 func orientation(p, q, r iPoint) int {
     64 	// See http://www.geeksforgeeks.org/orientation-3-ordered-points/
     65 	// for details of below formula.
     66 	collinear := 0
     67 	clockwise := 1
     68 	counterclockwise := 2
     69 
     70 	val := (q.GetY()-p.GetY())*(r.GetX()-q.GetX()) - (q.GetX()-p.GetX())*(r.GetY()-q.GetY())
     71 	if val == 0 { // collinear
     72 		return collinear
     73 	}
     74 	// clock or counterclockwise
     75 	if val > 0 {
     76 		return clockwise
     77 	}
     78 	return counterclockwise
     79 }
     80 
     81 // The main function that returns true if line segment 'p1q1'
     82 // and 'p2q2' intersect.
     83 func doIntersect(p1, q1, p2, q2 iPoint) bool {
     84 	// Find the four orientations needed for general and
     85 	// special cases
     86 	o1 := orientation(p1, q1, p2)
     87 	o2 := orientation(p1, q1, q2)
     88 	o3 := orientation(p2, q2, p1)
     89 	o4 := orientation(p2, q2, q1)
     90 	// General case
     91 	if o1 != o2 && o3 != o4 {
     92 		return true
     93 	}
     94 	// Special Cases
     95 	// p1, q1 and p2 are collinear and p2 lies on segment p1q1
     96 	if o1 == 0 && onSegment(p1, p2, q1) {
     97 		return true
     98 	}
     99 	// p1, q1 and p2 are collinear and q2 lies on segment p1q1
    100 	if o2 == 0 && onSegment(p1, q2, q1) {
    101 		return true
    102 	}
    103 	// p2, q2 and p1 are collinear and p1 lies on segment p2q2
    104 	if o3 == 0 && onSegment(p2, p1, q2) {
    105 		return true
    106 	}
    107 	// p2, q2 and q1 are collinear and q1 lies on segment p2q2
    108 	if o4 == 0 && onSegment(p2, q1, q2) {
    109 		return true
    110 	}
    111 	return false // Doesn't fall in any of the above cases
    112 }
    113 
    114 // Find intersection point
    115 func intersect(p0, p1, p2, p3 iPoint) Point {
    116 	var intersect Point
    117 	var s1 Point
    118 	var s2 Point
    119 
    120 	s1.pxY = p1.GetY() - p0.GetY()
    121 	s1.pxX = p1.GetX() - p0.GetX()
    122 	s2.pxY = p3.GetY() - p2.GetY()
    123 	s2.pxX = p3.GetX() - p2.GetX()
    124 
    125 	//s := (-s1.GetX()*(p0.GetY()-p2.GetY()) + s1.GetY()*(p0.GetX()-p2.GetX())) / (-s2.GetY()*s1.GetX() + s1.GetY()*s2.GetX())
    126 	t := (s2.GetY()*(p0.GetX()-p2.GetX()) - s2.GetX()*(p0.GetY()-p2.GetY())) / (-s2.GetY()*s1.GetX() + s1.GetY()*s2.GetX())
    127 
    128 	intersect.pxY = p0.GetY() + (t * s1.GetY())
    129 	intersect.pxX = p0.GetX() + (t * s1.GetX())
    130 
    131 	return intersect
    132 }
    133 
    134 // Given three collinear points p, q, r, the function checks if
    135 // point q lies on line segment 'pr'
    136 func onSegment(p, q, r iPoint) bool {
    137 	return q.GetY() <= math.Max(p.GetY(), r.GetY()) && q.GetY() >= math.Min(p.GetY(), r.GetY()) &&
    138 		q.GetX() <= math.Max(p.GetX(), r.GetX()) && q.GetX() >= math.Min(p.GetX(), r.GetX())
    139 }
    140 
    141 func colorDistance(e1, e2 color.RGBA) float64 {
    142 	r := float64(e1.R - e2.R)
    143 	g := float64(e1.G - e2.G)
    144 	b := float64(e1.B - e2.B)
    145 	return math.Sqrt(r*r + g*g + b*b)
    146 }
    147 
    148 func distance(p1, p2 iPoint) float64 {
    149 	return math.Sqrt(math.Pow(p2.GetX()-p1.GetX(), 2) + math.Pow(p2.GetY()-p1.GetY(), 2))
    150 }
    151 
    152 func angle(p1, p2 iPoint) float64 {
    153 	return math.Atan2(p1.GetY()-p2.GetY(), p1.GetX()-p2.GetX())
    154 }
    155 
    156 type Points []Point
    157 
    158 func (p Points) Pairs(clb func(i int, p1, p2 Point)) {
    159 	for i := 1; i < len(p); i++ {
    160 		p1 := p[i-1]
    161 		p2 := p[i]
    162 		clb(i, p1, p2)
    163 	}
    164 }
    165 
    166 type Point struct {
    167 	X   int
    168 	Y   int
    169 	pxX float64
    170 	pxY float64
    171 }
    172 
    173 func (p Point) GetX() float64 {
    174 	return p.pxX
    175 }
    176 
    177 func (p Point) GetY() float64 {
    178 	return p.pxY
    179 }
    180 
    181 // Get an int in range [from, to] which is different from "value"
    182 func (m *Image) getDifferentInt(value, from, to int) (out int) {
    183 	for {
    184 		out = m.RandInt(from, to)
    185 		if out != value {
    186 			return
    187 		}
    188 	}
    189 }
    190 
    191 type Path struct {
    192 	points []Point
    193 }
    194 
    195 func (p Path) coordTaken(row, col int) bool {
    196 	for _, pt := range p.points {
    197 		if pt.Y == row && pt.X == col {
    198 			return true
    199 		}
    200 	}
    201 	return false
    202 }
    203 
    204 func (p Path) calculateNbIntersections() (nbIntersects int) {
    205 	// Calculate nb intersections
    206 	for i := 3; i < len(p.points); i++ {
    207 		curr := p.points[i]
    208 		prev := p.points[i-1]
    209 		p1 := p.points[0]
    210 		for ii := 1; ii < i-1; ii++ {
    211 			p2 := p.points[ii]
    212 			if doIntersect(curr, prev, p1, p2) {
    213 				nbIntersects++
    214 			}
    215 			p1 = p2
    216 		}
    217 	}
    218 	return
    219 }
    220 
    221 func (p Path) checkIntersections() bool {
    222 	// Calculate nb intersections
    223 	for i := 3; i < len(p.points); i++ {
    224 		curr := p.points[i]
    225 		p1 := p.points[0]
    226 		for ii := 1; ii < i-1; ii++ {
    227 			p2 := p.points[ii]
    228 			if doIntersect(curr, curr, p1, p2) {
    229 				return false
    230 			}
    231 			p1 = p2
    232 		}
    233 	}
    234 	return true
    235 }
    236 
    237 func (p Path) checkOverlapping() bool {
    238 	// Calculate nb intersections
    239 	for i := 2; i < len(p.points); i++ {
    240 		curr := p.points[i]
    241 		p1 := p.points[i-1]
    242 		p2 := p.points[i-2]
    243 		if findAngle(curr, p1, p2) == 0 {
    244 			return false
    245 		}
    246 		if orientation(curr, p1, p2) == 0 {
    247 			return false
    248 		}
    249 		if onSegment(p1, p2, curr) {
    250 			return false
    251 		}
    252 	}
    253 	return true
    254 }
    255 
    256 func findAngle(p0, p1, c iPoint) float64 {
    257 	p0c := math.Sqrt(math.Pow(c.GetX()-p0.GetX(), 2) + math.Pow(c.GetY()-p0.GetY(), 2))
    258 	p1c := math.Sqrt(math.Pow(c.GetX()-p1.GetX(), 2) + math.Pow(c.GetY()-p1.GetY(), 2))
    259 	p0p1 := math.Sqrt(math.Pow(p1.GetX()-p0.GetX(), 2) + math.Pow(p1.GetY()-p0.GetY(), 2))
    260 	x := (p1c*p1c + p0c*p0c - p0p1*p0p1) / (2 * p1c * p0c)
    261 	if x > 1 {
    262 		x = 1
    263 	}
    264 	if x < -1 {
    265 		x = -1
    266 	}
    267 	return math.Acos(x)
    268 }
    269 
    270 func (p Path) intersectWith(p1, p2 iPoint) bool {
    271 	// Calculate nb intersections
    272 	for i := 1; i < len(p.points); i++ {
    273 		curr := p.points[i]
    274 		prev := p.points[i-1]
    275 		if doIntersect(curr, prev, p1, p2) {
    276 			return true
    277 		}
    278 	}
    279 	return false
    280 }
    281 
    282 func (m *Image) generateSemiValidPath(usedCoordsMap map[Point]struct{}) Path {
    283 	path := Path{}
    284 Loop:
    285 	for {
    286 		path = Path{}
    287 		path.points = make([]Point, 0)
    288 		row := -1
    289 		col := -1
    290 		var prevCoord Point
    291 		var prevTmp Point
    292 		for i := 0; i < m.nbDigits; i++ {
    293 			tmp := 0
    294 			for {
    295 				tmp++
    296 				if tmp > 100 {
    297 					continue Loop
    298 				}
    299 				prevTmp = prevCoord
    300 				row = m.getDifferentInt(prevTmp.Y, 0, 3)
    301 				col = m.getDifferentInt(prevTmp.X, 0, 4)
    302 				if path.coordTaken(row, col) {
    303 					continue
    304 				}
    305 				if _, found := usedCoordsMap[m.createPoint(col, row)]; found {
    306 					continue
    307 				}
    308 				curr := m.createPoint(col, row)
    309 				path.points = append(path.points, curr)
    310 				prevCoord = curr
    311 				break
    312 			}
    313 		}
    314 		break
    315 	}
    316 	return path
    317 }
    318 func (m *Image) generateValidPath(usedCoordsMap map[Point]struct{}) Path {
    319 	path := Path{}
    320 	for {
    321 		path = m.generateSemiValidPath(usedCoordsMap)
    322 		if !path.checkIntersections() {
    323 			continue
    324 		}
    325 		if !path.checkOverlapping() {
    326 			continue
    327 		}
    328 		nbIntersects := path.calculateNbIntersections()
    329 		// Acceptable captcha if we have at least 1 intersection
    330 		if nbIntersects > 1 {
    331 			break
    332 		}
    333 	}
    334 	return path
    335 }
    336 
    337 func (m *Image) createPoint(col, row int) Point {
    338 	return Point{X: col, Y: row,
    339 		pxX: float64(m.borderLeft+col*(m.numWidth+m.digitSpacing)) + (float64(m.numWidth) / 2) + m.RandFloat(-m.centerOffset, m.centerOffset),
    340 		pxY: float64(m.borderTop+row*(m.numHeight+m.digitVertSpacing)) + (float64(m.numHeight) / 2) + m.RandFloat(-m.centerOffset, m.centerOffset)}
    341 }
    342 
    343 func RandChoice[T any](rnd *rand.Rand, arr []T) T {
    344 	if len(arr) == 0 {
    345 		panic("empty array")
    346 	}
    347 	return arr[rnd.Intn(len(arr))]
    348 }
    349 
    350 func (m *Image) RandInt(min, max int) int {
    351 	if min == max {
    352 		return min
    353 	}
    354 	if max < min {
    355 		min, max = max, min
    356 	}
    357 	return m.rnd.Intn(max-min+1) + min
    358 }
    359 
    360 func (m *Image) RandFloat(min, max float64) float64 {
    361 	if min == max {
    362 		return min
    363 	}
    364 	if max < min {
    365 		min, max = max, min
    366 	}
    367 	return m.rnd.Float64()*(max-min) + min
    368 }
    369 
    370 var signatureFace = loadSignatureFace()
    371 var faces = loadFontFaces()
    372 
    373 func NewImage(answer string, difficulty int64, rnd *rand.Rand, isHelpImg bool) *Image {
    374 	//start := time.Now()
    375 	//defer func() { fmt.Println("took:", time.Since(start)) }()
    376 
    377 	m := new(Image)
    378 	m.rnd = rnd
    379 	m.renderHelpImg = isHelpImg
    380 	m.displayDebug = false
    381 	m.imageWidth = 240
    382 	m.imageHeight = 120
    383 	m.numWidth = 11
    384 	m.numHeight = 18
    385 	m.nbNumRows = 4
    386 	m.nbNumCols = 5
    387 	m.centerOffset = 0.0
    388 	m.nbDigits = 6
    389 	m.borderTop = 7
    390 	m.borderLeft = 25
    391 	m.digitSpacing = 35
    392 	m.digitVertSpacing = 12
    393 	m.difficulty = difficulty
    394 
    395 	if m.difficulty == 1 {
    396 		m.CubicDelta = 0
    397 	} else {
    398 		m.CubicDelta = 20
    399 	}
    400 
    401 	updateUsedCoordsMap := func(path Path, usedCoordsMap map[Point]struct{}) {
    402 		for _, pt := range path.points {
    403 			usedCoordsMap[pt] = struct{}{}
    404 		}
    405 	}
    406 
    407 	usedCoordsMap := make(map[Point]struct{})
    408 	m.mainPath = m.generateValidPath(usedCoordsMap)
    409 	updateUsedCoordsMap(m.mainPath, usedCoordsMap)
    410 	m.secondPath = m.generateSemiValidPath(usedCoordsMap)
    411 	updateUsedCoordsMap(m.secondPath, usedCoordsMap)
    412 	m.thirdPath = m.generateSemiValidPath(usedCoordsMap)
    413 	updateUsedCoordsMap(m.thirdPath, usedCoordsMap)
    414 
    415 	alphabet := ALPHABET
    416 
    417 	// Generate all numbers
    418 	m.numbersMatrix = make([][]Number, 0)
    419 	for row := 0; row < m.nbNumRows; row++ {
    420 		numbers := make([]Number, 0)
    421 		for col := 0; col < m.nbNumCols; col++ {
    422 			d := m.RandInt(0, len(alphabet)-1)
    423 			opacity := uint8(255)
    424 			if m.renderHelpImg {
    425 				opacity = 100
    426 			}
    427 			faceIdx := m.RandInt(0, len(faces)-1)
    428 			numbers = append(numbers, Number{
    429 				Num:     alphabet[d],
    430 				Angle:   m.RandFloat(-40, 40),
    431 				FaceIdx: faceIdx,
    432 				Face:    faces[faceIdx],
    433 				Opacity: opacity})
    434 		}
    435 		m.numbersMatrix = append(m.numbersMatrix, numbers)
    436 	}
    437 
    438 	// Replace numbers by the answer digits
    439 	for i, c := range m.mainPath.points {
    440 		d := answer[i]
    441 		// 7 with negative angle looks like 1
    442 		if d == 7 {
    443 			m.numbersMatrix[c.Y][c.X].Angle = m.RandFloat(0, 40)
    444 		}
    445 		m.numbersMatrix[c.Y][c.X].Opacity = 255
    446 		m.numbersMatrix[c.Y][c.X].Num = d
    447 	}
    448 
    449 	m.render()
    450 	return m
    451 }
    452 
    453 func (m *Image) getLineColor() color.RGBA {
    454 	min := 80
    455 	max := 255
    456 	return color.RGBA{R: uint8(m.RandInt(min, max)), G: uint8(m.RandInt(min, max)), B: uint8(m.RandInt(min, max)), A: 255}
    457 }
    458 
    459 func loadFontFaces() (out []font1.Face) {
    460 	loadFF := func(path string, size float64) {
    461 		fontBytes := bindata.MustAsset(path)
    462 		f, err := truetype.Parse(fontBytes)
    463 		if err != nil {
    464 			logrus.Error(err)
    465 		}
    466 		face := truetype.NewFace(f, &truetype.Options{Size: size})
    467 		out = append(out, face)
    468 	}
    469 	loadFF("font/Lato-Regular.ttf", 25)
    470 	loadFF("font/JessicaGroovyBabyFINAL2.ttf", 25)
    471 	loadFF("font/PineappleDelight.ttf", 25)
    472 	loadFF("font/df66c.ttf", 20)
    473 	loadFF("font/agengsans.ttf", 30)
    474 	return
    475 }
    476 
    477 func loadSignatureFace() font1.Face {
    478 	fontBytes := bindata.MustAsset("font/Lato-Regular.ttf")
    479 	f, _ := truetype.Parse(fontBytes)
    480 	return truetype.NewFace(f, &truetype.Options{Size: 10})
    481 }
    482 
    483 func (m *Image) withState(clb func()) {
    484 	m.c.Push()
    485 	defer m.c.Pop()
    486 	clb()
    487 }
    488 
    489 var blackClr = color.RGBA{R: 15, G: 15, B: 15, A: 255}
    490 
    491 // This is a hack because github.com/golang/freetype is not thread safe and will crash the program.
    492 // So we can only render 1 captcha at the time.
    493 // https://github.com/golang/freetype/issues/65
    494 // Can reproduce the bug by launching two terminals that runs the following script
    495 /**
    496 #!/bin/bash
    497 for i in {1..100}
    498 do
    499    curl -s "http://127.0.0.1:8080/bhc?username=n0tr1v" > /dev/null
    500 done
    501 */
    502 var renderMutex = sync.Mutex{}
    503 
    504 func (m *Image) render() {
    505 	m.RGBA = image.NewRGBA(image.Rect(0, 0, m.imageWidth, m.imageHeight))
    506 	m.c = gg.NewContextForRGBA(m.RGBA)
    507 
    508 	// This hack should never be necessary since we have the renderMutex
    509 	defer func() {
    510 		if r := recover(); r != nil {
    511 			logrus.Error("Failed to create captcha (font concurrency bug)")
    512 		}
    513 	}()
    514 
    515 	renderMutex.Lock()
    516 	defer renderMutex.Unlock()
    517 	m.withState(func() {
    518 		m.renderBackground()
    519 		m.renderTrianglesPattern()
    520 		//m.renderUselessLines()
    521 		m.renderDigits(0, color.RGBA{R: 255, G: 255, B: 255, A: 255})
    522 		m.renderOrphanNumbers()
    523 		m.renderFakePath(m.secondPath.points)
    524 		m.renderFakePath(m.thirdPath.points)
    525 		m.renderPath(m.mainPath.points)
    526 		m.renderDigits(1, color.RGBA{R: 255, G: 255, B: 255, A: 100})
    527 		m.renderDebugGrid()
    528 		m.renderSignature()
    529 		m.renderBorder()
    530 	})
    531 }
    532 
    533 func (m *Image) renderOrphanNumbers() {
    534 	points := make([]Point, 0)
    535 	for row := 0; row < m.nbNumRows; row++ {
    536 		for col := 0; col < m.nbNumCols; col++ {
    537 			if m.mainPath.coordTaken(row, col) {
    538 				continue
    539 			}
    540 			if m.secondPath.coordTaken(row, col) {
    541 				continue
    542 			}
    543 			if m.thirdPath.coordTaken(row, col) {
    544 				continue
    545 			}
    546 			pt := m.createPoint(col, row)
    547 			points = append(points, pt)
    548 		}
    549 	}
    550 	m.renderFakePath(points)
    551 }
    552 
    553 func (m *Image) renderUselessLines() {
    554 	d := 20.0
    555 	m.withState(func() {
    556 		for i := 0; i < 3; i++ {
    557 			d = RandChoice(m.rnd, []float64{d, -d})
    558 			pt1 := Point{pxX: 0, pxY: m.RandFloat(-50, 30+float64(m.imageHeight))}
    559 			pt2 := Point{pxX: float64(m.imageWidth), pxY: m.RandFloat(-30, 50+float64(m.imageHeight))}
    560 			m.renderPath([]Point{pt1, pt2})
    561 		}
    562 		for i := 0; i < 3; i++ {
    563 			d = RandChoice(m.rnd, []float64{d, -d})
    564 			pt1 := Point{pxX: 0, pxY: m.RandFloat(-30, 30+float64(m.imageHeight))}
    565 			pt2 := Point{pxX: float64(m.imageWidth), pxY: m.RandFloat(-30, 30+float64(m.imageHeight))}
    566 			grad := getGradient1(pt1, pt2, m.getLineColor(), m.getLineColor(), m.getLineColor())
    567 			m.c.SetStrokeStyle(grad)
    568 			m.c.SetDash(5, 3)
    569 			m.c.SetLineWidth(1)
    570 			m.strokeCubicLine(pt1, pt2, d)
    571 		}
    572 		for i := 0; i < 3; i++ {
    573 			d = RandChoice(m.rnd, []float64{d, -d})
    574 			pt1 := Point{pxY: 0, pxX: m.RandFloat(-30, 30+float64(m.imageWidth))}
    575 			pt2 := Point{pxY: float64(m.imageHeight), pxX: m.RandFloat(-30, 30+float64(m.imageHeight))}
    576 			grad := getGradient1(pt1, pt2, m.getLineColor(), m.getLineColor(), m.getLineColor())
    577 			m.c.SetStrokeStyle(grad)
    578 			m.c.SetDash(5, 3)
    579 			m.c.SetLineWidth(1)
    580 			m.strokeCubicLine(pt1, pt2, d)
    581 		}
    582 	})
    583 }
    584 
    585 func getGradient(p1, p2 Point, gradColors []color.RGBA, i int) gg.Gradient {
    586 	c1 := gradColors[(i*2)-2]
    587 	c2 := gradColors[(i*2)-1]
    588 	c3 := gradColors[i*2]
    589 	return getGradient1(p1, p2, c1, c2, c3)
    590 }
    591 
    592 func getGradient1(p1, p2 Point, c1, c2, c3 color.RGBA) gg.Gradient {
    593 	grad := gg.NewLinearGradient(p1.GetX(), p1.GetY(), p2.GetX(), p2.GetY())
    594 	grad.AddColorStop(0, c1)
    595 	grad.AddColorStop(0.5, c2)
    596 	grad.AddColorStop(1, c3)
    597 	return grad
    598 }
    599 
    600 func (m *Image) getGradientColors(nbPoints int) []color.RGBA {
    601 	gradColors := make([]color.RGBA, nbPoints*2)
    602 	for i := 0; i < nbPoints*2; i += 2 {
    603 		gradColors[i] = m.getLineColor()
    604 		gradColors[i+1] = m.getLineColor()
    605 	}
    606 	return gradColors
    607 }
    608 
    609 func (m *Image) strokeCubicLine(p1, p2 Point, d float64) {
    610 	m.c.MoveTo(p1.GetX(), p1.GetY())
    611 	m.c.CubicTo(p1.GetX()+d, p1.GetY()+d, p2.GetX()-d, p2.GetY()-d, p2.GetX(), p2.GetY())
    612 	m.c.Stroke()
    613 }
    614 
    615 func (m *Image) renderPath(points Points) {
    616 	d := m.CubicDelta
    617 
    618 	gradColors := m.getGradientColors(len(points))
    619 
    620 	// Draw the whole line
    621 	m.withState(func() {
    622 		points.Pairs(func(i int, prev, pt Point) {
    623 			grad := getGradient(prev, pt, gradColors, i)
    624 			m.c.SetStrokeStyle(grad)
    625 			//m.c.SetColor(color.White)
    626 
    627 			m.withState(func() {
    628 				m.c.SetLineWidth(4)
    629 				m.c.SetColor(blackClr)
    630 				m.strokeCubicLine(prev, pt, d)
    631 			})
    632 
    633 			m.withState(func() {
    634 				m.c.SetLineWidth(1.5)
    635 				m.strokeCubicLine(prev, pt, d)
    636 			})
    637 		})
    638 	})
    639 
    640 	m.withState(func() {
    641 		m.c.SetLineWidth(1.5)
    642 		m.c.SetDash(10, 300)
    643 		points.Pairs(func(i int, prev, pt Point) {
    644 			grad := getGradient(prev, pt, gradColors, i)
    645 			m.c.SetStrokeStyle(grad)
    646 			//m.c.SetColor(color.RGBA{255, 0, 0, 255})
    647 
    648 			m.strokeCubicLine(prev, pt, d)
    649 
    650 			if i == len(points)-1 {
    651 				m.withState(func() {
    652 					m.c.SetLineWidth(4)
    653 					m.c.SetColor(blackClr)
    654 					m.strokeCubicLine(pt, prev, -d)
    655 				})
    656 
    657 				m.withState(func() {
    658 					m.c.SetDashOffset(4)
    659 					m.c.SetDash(30, 300)
    660 					m.strokeCubicLine(pt, prev, -d)
    661 				})
    662 			} else {
    663 				m.withState(func() {
    664 					m.strokeCubicLine(pt, prev, -d)
    665 				})
    666 			}
    667 		})
    668 	})
    669 }
    670 
    671 func (m *Image) renderFakePath(points Points) {
    672 	if m.renderHelpImg {
    673 		return
    674 	}
    675 
    676 	d := m.CubicDelta
    677 	gradColors := m.getGradientColors(len(points))
    678 
    679 	// Draw the whole line
    680 	m.withState(func() {
    681 		m.c.SetLineWidth(1)
    682 		points.Pairs(func(i int, prev, pt Point) {
    683 			grad := getGradient(prev, pt, gradColors, i)
    684 			m.c.SetStrokeStyle(grad)
    685 			m.strokeCubicLine(prev, pt, -d)
    686 		})
    687 	})
    688 
    689 	// Semi transparent black line on top of the line
    690 	m.withState(func() {
    691 		m.c.SetLineWidth(4)
    692 		m.c.SetColor(color.RGBA{R: 15, G: 15, B: 15, A: 100})
    693 		points.Pairs(func(i int, prev, pt Point) {
    694 			m.strokeCubicLine(prev, pt, -d)
    695 			m.strokeCubicLine(pt, prev, d)
    696 		})
    697 	})
    698 
    699 	// Draw the whole line again with dashes
    700 	m.withState(func() {
    701 		m.c.SetDash(5, 3)
    702 		points.Pairs(func(i int, prev, pt Point) {
    703 			grad := getGradient(prev, pt, gradColors, i)
    704 			m.c.SetStrokeStyle(grad)
    705 
    706 			m.withState(func() {
    707 				m.c.SetLineWidth(4)
    708 				m.c.SetColor(blackClr)
    709 				m.strokeCubicLine(prev, pt, -d)
    710 			})
    711 
    712 			m.withState(func() {
    713 				m.c.SetLineWidth(1.5)
    714 				m.strokeCubicLine(prev, pt, -d)
    715 			})
    716 		})
    717 	})
    718 
    719 	// Draw line edges with longer dashes
    720 	m.withState(func() {
    721 		m.c.SetDash(30, 200)
    722 		m.c.SetLineWidth(1.5)
    723 		points.Pairs(func(i int, prev, pt Point) {
    724 			grad := getGradient(prev, pt, gradColors, i)
    725 			m.c.SetStrokeStyle(grad)
    726 
    727 			m.strokeCubicLine(prev, pt, -d)
    728 			m.strokeCubicLine(pt, prev, d)
    729 		})
    730 	})
    731 }
    732 
    733 func (m *Image) renderBackground() {
    734 	m.withState(func() {
    735 		//grad := gg.NewLinearGradient(0, 0, float64(m.imageWidth), float64(m.imageHeight))
    736 		//grad.AddColorStop(0, color.RGBA{10, 10, 10, 255})
    737 		//grad.AddColorStop(1, color.RGBA{50, 50, 50, 255})
    738 		//m.c.SetFillStyle(grad)
    739 		m.c.SetColor(blackClr)
    740 		m.c.DrawRectangle(0, 0, float64(m.imageWidth), float64(m.imageHeight))
    741 		m.c.Fill()
    742 	})
    743 }
    744 
    745 func (m *Image) renderBorder() {
    746 	m.withState(func() {
    747 		m.c.SetColor(blackClr)
    748 		m.c.DrawRectangle(0, 0, float64(m.imageWidth)-0.5, float64(m.imageHeight)-0.5)
    749 		m.c.Stroke()
    750 	})
    751 }
    752 
    753 func (m *Image) renderSquaresPattern() {
    754 	y := 0.0
    755 	for y < float64(m.imageHeight) {
    756 		x := 0.0
    757 		for x < float64(m.imageWidth) {
    758 			num := uint8(m.RandInt(20, 40))
    759 			m.c.SetColor(color.RGBA{R: num, G: num, B: num, A: 255})
    760 			m.c.DrawRectangle(x, y, 10, 10)
    761 			m.c.Fill()
    762 			x += 11
    763 		}
    764 		y += 11
    765 	}
    766 }
    767 
    768 func (m *Image) renderTrianglesPattern() {
    769 	xStartRandOffset := m.RandFloat(0, 22)
    770 	yStartRandOffset := m.RandFloat(0, 3)
    771 	y := -yStartRandOffset
    772 	for i := 0; i < 8; i++ {
    773 		x := -11 - xStartRandOffset
    774 		if i%2 == 0 {
    775 			x -= 11
    776 		}
    777 		for j := 0; j < 13; j++ {
    778 			m.withState(func() {
    779 				m.c.Translate(x, y)
    780 				num := uint8(m.RandInt(20, 40))
    781 				m.c.SetColor(color.RGBA{R: num, G: num, B: num, A: 255})
    782 				m.c.NewSubPath()
    783 				m.c.MoveTo(0, 0)
    784 				m.c.LineTo(10, 17)
    785 				m.c.LineTo(20, 0)
    786 				m.c.ClosePath()
    787 				m.c.Fill()
    788 
    789 				m.c.NewSubPath()
    790 				m.c.MoveTo(11, 17)
    791 				m.c.LineTo(21, 0)
    792 				m.c.LineTo(31, 17)
    793 				m.c.ClosePath()
    794 				m.c.Fill()
    795 			})
    796 			x += 22
    797 		}
    798 		y += 18
    799 	}
    800 }
    801 
    802 func (m *Image) renderDebugGrid() {
    803 	if !m.displayDebug {
    804 		return
    805 	}
    806 	m.withState(func() {
    807 		m.c.SetLineWidth(1)
    808 		m.c.SetColor(color.RGBA{R: 100, G: 100, B: 100, A: 255})
    809 		for col := 0; col <= m.nbNumCols; col++ {
    810 			x := float64(m.borderLeft+col*(m.numWidth+m.digitSpacing)) - float64(m.numHeight) - 0.5
    811 			m.c.MoveTo(x, 0)
    812 			m.c.LineTo(x, float64(m.imageHeight))
    813 		}
    814 		for row := 0; row <= m.nbNumRows; row++ {
    815 			y := float64(m.borderTop+row*(m.numHeight+m.digitVertSpacing)) - 6 - 0.5
    816 			m.c.MoveTo(0, y)
    817 			m.c.LineTo(float64(m.imageWidth), y)
    818 		}
    819 		m.c.Stroke()
    820 	})
    821 }
    822 
    823 func (m *Image) renderDigits(idx int64, clr color.RGBA) {
    824 	m.withState(func() {
    825 		x := m.borderLeft
    826 		y := m.borderTop
    827 		for row := 0; row < m.nbNumRows; row++ {
    828 			for col := 0; col < m.nbNumCols; col++ {
    829 				m.withState(func() {
    830 					num := m.numbersMatrix[row][col]
    831 					if idx == 0 {
    832 						clr.A = num.Opacity
    833 					}
    834 					m.c.SetColor(clr)
    835 					m.c.Translate(float64(x+m.numWidth/2), float64(y+m.numHeight/2))
    836 					m.c.Rotate(gg.Radians(num.Angle))
    837 					m.c.SetFontFace(num.Face)
    838 					m.c.DrawString(string(num.Num), float64(-m.numWidth/2), float64(m.numHeight/2))
    839 					//m.withState(func() {
    840 					//	m.c.SetColor(color.RGBA{255, 0, 0, 255})
    841 					//	m.c.DrawCircle(0, 0, 2)
    842 					//	m.c.Fill()
    843 					//})
    844 				})
    845 				x += m.numWidth + m.digitSpacing
    846 			}
    847 			x = m.borderLeft
    848 			y += m.numHeight + m.digitVertSpacing
    849 		}
    850 	})
    851 }
    852 
    853 func (m *Image) renderSignature() {
    854 	m.withState(func() {
    855 		m.c.SetFontFace(signatureFace)
    856 		w, h := m.c.MeasureString("n0tr1v")
    857 		num := uint8(50)
    858 		m.c.SetColor(color.RGBA{R: num, G: num, B: num, A: 255})
    859 		m.c.Translate(float64(m.imageWidth)/2, float64(m.imageHeight)/2)
    860 		m.c.Rotate(-math.Pi / 2)
    861 		m.c.Translate(-float64(m.numHeight/2)-w-15, float64(m.imageWidth)/2-h-1)
    862 		m.c.DrawString("n0tr1v", float64(-m.numWidth/2), float64(m.numHeight/2))
    863 	})
    864 }
    865 
    866 // encodedPNG encodes an image to PNG and returns
    867 // the result as a byte slice.
    868 func (m *Image) encodedPNG() []byte {
    869 	var buf bytes.Buffer
    870 	if err := png.Encode(&buf, m.RGBA); err != nil {
    871 		panic(err.Error())
    872 	}
    873 	return buf.Bytes()
    874 }
    875 
    876 // WriteTo writes captcha image in PNG format into the given writer.
    877 func (m *Image) WriteTo(w io.Writer) (int64, error) {
    878 	n, err := w.Write(m.encodedPNG())
    879 	return int64(n), err
    880 }