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 }