image.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. package captcha
  2. import (
  3. "image"
  4. "image/png"
  5. "io"
  6. "os"
  7. "rand"
  8. "time"
  9. )
  10. const (
  11. // Standard width and height of a captcha image.
  12. StdWidth = 300
  13. StdHeight = 80
  14. maxSkew = 2
  15. )
  16. type Image struct {
  17. *image.NRGBA
  18. primaryColor image.NRGBAColor
  19. numWidth int
  20. numHeight int
  21. dotSize int
  22. }
  23. func init() {
  24. rand.Seed(time.Seconds())
  25. }
  26. // NewImage returns a new captcha image of the given width and height with the
  27. // given digits, where each digit must be in range 0-9.
  28. func NewImage(digits []byte, width, height int) *Image {
  29. img := new(Image)
  30. img.NRGBA = image.NewNRGBA(width, height)
  31. img.primaryColor = image.NRGBAColor{
  32. uint8(rand.Intn(129)),
  33. uint8(rand.Intn(129)),
  34. uint8(rand.Intn(129)),
  35. 0xFF,
  36. }
  37. img.calculateSizes(width, height, len(digits))
  38. // Draw background (10 random circles of random brightness).
  39. img.fillWithCircles(10, img.dotSize)
  40. // Randomly position captcha inside the image.
  41. maxx := width - (img.numWidth+img.dotSize)*len(digits) - img.dotSize
  42. maxy := height - img.numHeight - img.dotSize*2
  43. x := rnd(img.dotSize*2, maxx)
  44. y := rnd(img.dotSize*2, maxy)
  45. // Draw digits.
  46. for _, n := range digits {
  47. img.drawDigit(font[n], x, y)
  48. x += img.numWidth + img.dotSize
  49. }
  50. // Draw strike-through line.
  51. img.strikeThrough()
  52. return img
  53. }
  54. // BUG(dchest): While Image conforms to io.WriterTo interface, its WriteTo
  55. // method returns 0 instead of the actual bytes written because png.Encode
  56. // doesn't report this.
  57. // WriteTo writes captcha image in PNG format into the given writer.
  58. func (img *Image) WriteTo(w io.Writer) (int64, os.Error) {
  59. return 0, png.Encode(w, img)
  60. }
  61. func (img *Image) calculateSizes(width, height, ncount int) {
  62. // Goal: fit all digits inside the image.
  63. var border int
  64. if width > height {
  65. border = height / 5
  66. } else {
  67. border = width / 5
  68. }
  69. // Convert everything to floats for calculations.
  70. w := float64(width - border*2)
  71. h := float64(height - border*2)
  72. // fw takes into account 1-dot spacing between digits.
  73. fw := float64(fontWidth) + 1
  74. fh := float64(fontHeight)
  75. nc := float64(ncount)
  76. // Calculate the width of a single digit taking into account only the
  77. // width of the image.
  78. nw := w / nc
  79. // Calculate the height of a digit from this width.
  80. nh := nw * fh / fw
  81. // Digit too high?
  82. if nh > h {
  83. // Fit digits based on height.
  84. nh = h
  85. nw = fw / fh * nh
  86. }
  87. // Calculate dot size.
  88. img.dotSize = int(nh / fh)
  89. // Save everything, making the actual width smaller by 1 dot to account
  90. // for spacing between digits.
  91. img.numWidth = int(nw)
  92. img.numHeight = int(nh) - img.dotSize
  93. }
  94. func (img *Image) drawHorizLine(color image.Color, fromX, toX, y int) {
  95. for x := fromX; x <= toX; x++ {
  96. img.Set(x, y, color)
  97. }
  98. }
  99. func (img *Image) drawCircle(color image.Color, x, y, radius int) {
  100. f := 1 - radius
  101. dfx := 1
  102. dfy := -2 * radius
  103. xx := 0
  104. yy := radius
  105. img.Set(x, y+radius, color)
  106. img.Set(x, y-radius, color)
  107. img.drawHorizLine(color, x-radius, x+radius, y)
  108. for xx < yy {
  109. if f >= 0 {
  110. yy--
  111. dfy += 2
  112. f += dfy
  113. }
  114. xx++
  115. dfx += 2
  116. f += dfx
  117. img.drawHorizLine(color, x-xx, x+xx, y+yy)
  118. img.drawHorizLine(color, x-xx, x+xx, y-yy)
  119. img.drawHorizLine(color, x-yy, x+yy, y+xx)
  120. img.drawHorizLine(color, x-yy, x+yy, y-xx)
  121. }
  122. }
  123. func (img *Image) fillWithCircles(n, maxradius int) {
  124. color := img.primaryColor
  125. maxx := img.Bounds().Max.X
  126. maxy := img.Bounds().Max.Y
  127. for i := 0; i < n; i++ {
  128. setRandomBrightness(&color, 255)
  129. r := rnd(1, maxradius)
  130. img.drawCircle(color, rnd(r, maxx-r), rnd(r, maxy-r), r)
  131. }
  132. }
  133. func (img *Image) strikeThrough() {
  134. r := 0
  135. maxx := img.Bounds().Max.X
  136. maxy := img.Bounds().Max.Y
  137. y := rnd(maxy/3, maxy-maxy/3)
  138. for x := 0; x < maxx; x += r {
  139. r = rnd(1, img.dotSize/3)
  140. y += rnd(-img.dotSize/2, img.dotSize/2)
  141. if y <= 0 || y >= maxy {
  142. y = rnd(maxy/3, maxy-maxy/3)
  143. }
  144. img.drawCircle(img.primaryColor, x, y, r)
  145. }
  146. }
  147. func (img *Image) drawDigit(digit []byte, x, y int) {
  148. skf := rndf(-maxSkew, maxSkew)
  149. xs := float64(x)
  150. minr := img.dotSize / 2 // minumum radius
  151. maxr := img.dotSize/2 + img.dotSize/4 // maximum radius
  152. y += rnd(-minr, minr)
  153. for yy := 0; yy < fontHeight; yy++ {
  154. for xx := 0; xx < fontWidth; xx++ {
  155. if digit[yy*fontWidth+xx] != blackChar {
  156. continue
  157. }
  158. // Introduce random variations.
  159. or := rnd(minr, maxr)
  160. ox := x + (xx * img.dotSize) + rnd(0, or/2)
  161. oy := y + (yy * img.dotSize) + rnd(0, or/2)
  162. img.drawCircle(img.primaryColor, ox, oy, or)
  163. }
  164. xs += skf
  165. x = int(xs)
  166. }
  167. }
  168. func setRandomBrightness(c *image.NRGBAColor, max uint8) {
  169. minc := min3(c.R, c.G, c.B)
  170. maxc := max3(c.R, c.G, c.B)
  171. if maxc > max {
  172. return
  173. }
  174. n := rand.Intn(int(max-maxc)) - int(minc)
  175. c.R = uint8(int(c.R) + n)
  176. c.G = uint8(int(c.G) + n)
  177. c.B = uint8(int(c.B) + n)
  178. }
  179. func min3(x, y, z uint8) (o uint8) {
  180. o = x
  181. if y < o {
  182. o = y
  183. }
  184. if z < o {
  185. o = z
  186. }
  187. return
  188. }
  189. func max3(x, y, z uint8) (o uint8) {
  190. o = x
  191. if y > o {
  192. o = y
  193. }
  194. if z > o {
  195. o = z
  196. }
  197. return
  198. }
  199. // rnd returns a random number in range [from, to].
  200. func rnd(from, to int) int {
  201. return rand.Intn(to+1-from) + from
  202. }